diff --git a/cmd/garth/activities.go b/cmd/garth/activities.go index 27fd785..4eb99c2 100644 --- a/cmd/garth/activities.go +++ b/cmd/garth/activities.go @@ -15,7 +15,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/pkg/garmin" ) var ( diff --git a/cmd/garth/auth.go b/cmd/garth/auth.go index e589070..c2bed2d 100644 --- a/cmd/garth/auth.go +++ b/cmd/garth/auth.go @@ -6,7 +6,7 @@ import ( "golang.org/x/term" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/pkg/garmin" "github.com/spf13/cobra" ) diff --git a/cmd/garth/cmd/activities.go b/cmd/garth/cmd/activities.go index 19e2a68..185e19b 100644 --- a/cmd/garth/cmd/activities.go +++ b/cmd/garth/cmd/activities.go @@ -5,8 +5,8 @@ import ( "log" "time" - "go-garth/internal/auth/credentials" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/internal/auth/credentials" + "github.com/sstent/go-garth/pkg/garmin" "github.com/spf13/cobra" ) diff --git a/cmd/garth/cmd/data.go b/cmd/garth/cmd/data.go index b07d30b..e6d1499 100644 --- a/cmd/garth/cmd/data.go +++ b/cmd/garth/cmd/data.go @@ -7,8 +7,8 @@ import ( "os" "time" - "go-garth/internal/auth/credentials" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/internal/auth/credentials" + "github.com/sstent/go-garth/pkg/garmin" "github.com/spf13/cobra" ) diff --git a/cmd/garth/cmd/stats.go b/cmd/garth/cmd/stats.go index afea464..505249c 100644 --- a/cmd/garth/cmd/stats.go +++ b/cmd/garth/cmd/stats.go @@ -4,8 +4,8 @@ import ( "log" "time" - "go-garth/internal/auth/credentials" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/internal/auth/credentials" + "github.com/sstent/go-garth/pkg/garmin" "github.com/spf13/cobra" ) diff --git a/cmd/garth/cmd/tokens.go b/cmd/garth/cmd/tokens.go index 700dd77..4e1accf 100644 --- a/cmd/garth/cmd/tokens.go +++ b/cmd/garth/cmd/tokens.go @@ -5,8 +5,8 @@ import ( "fmt" "log" - "go-garth/internal/auth/credentials" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/internal/auth/credentials" + "github.com/sstent/go-garth/pkg/garmin" "github.com/spf13/cobra" ) diff --git a/cmd/garth/config.go b/cmd/garth/config.go index a61532d..a11dcdb 100644 --- a/cmd/garth/config.go +++ b/cmd/garth/config.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" "gopkg.in/yaml.v3" - "go-garth/internal/config" + "github.com/sstent/go-garth/internal/config" ) func init() { diff --git a/cmd/garth/health.go b/cmd/garth/health.go index 7d3f280..1c7182f 100644 --- a/cmd/garth/health.go +++ b/cmd/garth/health.go @@ -12,9 +12,9 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "go-garth/internal/data" // Import the data package - types "go-garth/internal/models/types" - "go-garth/pkg/garmin" + "github.com/sstent/go-garth/internal/data" // Import the data package + types "github.com/sstent/go-garth/internal/models/types" + "github.com/sstent/go-garth/pkg/garmin" ) var ( diff --git a/cmd/garth/root.go b/cmd/garth/root.go index c133e3c..446758a 100644 --- a/cmd/garth/root.go +++ b/cmd/garth/root.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "go-garth/internal/config" + "github.com/sstent/go-garth/internal/config" ) var ( diff --git a/cmd/garth/stats.go b/cmd/garth/stats.go index 989daaa..32685e6 100644 --- a/cmd/garth/stats.go +++ b/cmd/garth/stats.go @@ -11,8 +11,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - types "go-garth/internal/models/types" - "go-garth/pkg/garmin" + types "github.com/sstent/go-garth/internal/models/types" + "github.com/sstent/go-garth/pkg/garmin" ) var ( diff --git a/garth b/garth new file mode 100755 index 0000000..069a613 Binary files /dev/null and b/garth differ diff --git a/go.mod b/go.mod index 6e38cfd..a62f011 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go-garth +module github.com/sstent/go-garth go 1.24.2 diff --git a/internal/api/client/auth_test.go b/internal/api/client/auth_test.go index 7f4255a..61d2a93 100644 --- a/internal/api/client/auth_test.go +++ b/internal/api/client/auth_test.go @@ -3,8 +3,8 @@ package client_test import ( "testing" - "go-garth/internal/api/client" - "go-garth/internal/auth/credentials" + "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/auth/credentials" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/internal/api/client/client.go b/internal/api/client/client.go index c3a1302..82a98de 100644 --- a/internal/api/client/client.go +++ b/internal/api/client/client.go @@ -14,11 +14,11 @@ import ( "strings" "time" - "go-garth/internal/auth/sso" - "go-garth/internal/errors" - types "go-garth/internal/models/types" - shared "go-garth/shared/interfaces" - models "go-garth/shared/models" + "github.com/sstent/go-garth/internal/auth/sso" + "github.com/sstent/go-garth/internal/errors" + types "github.com/sstent/go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" + models "github.com/sstent/go-garth/shared/models" ) // Client represents the Garmin Connect API client diff --git a/internal/api/client/client_test.go b/internal/api/client/client_test.go index 25e364c..f9a78eb 100644 --- a/internal/api/client/client_test.go +++ b/internal/api/client/client_test.go @@ -7,12 +7,12 @@ import ( "testing" "time" - "go-garth/internal/testutils" + "github.com/sstent/go-garth/internal/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/api/client" ) func TestClient_GetUserProfile(t *testing.T) { diff --git a/internal/auth/credentials/credentials.go b/internal/auth/credentials/credentials.go index d856078..df27b1a 100644 --- a/internal/auth/credentials/credentials.go +++ b/internal/auth/credentials/credentials.go @@ -11,7 +11,7 @@ import ( // LoadEnvCredentials loads credentials from .env file func LoadEnvCredentials() (email, password, domain string, err error) { // Determine project root (assuming .env is in the project root) - projectRoot := "/home/sstent/Projects/go-garth" + projectRoot := "/home/sstent/Projects/github.com/sstent/go-garth" envPath := filepath.Join(projectRoot, ".env") // Load .env file diff --git a/internal/auth/oauth/oauth.go b/internal/auth/oauth/oauth.go index edd6d1b..06ff18e 100644 --- a/internal/auth/oauth/oauth.go +++ b/internal/auth/oauth/oauth.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "go-garth/internal/models/types" - "go-garth/internal/utils" + "github.com/sstent/go-garth/internal/models/types" + "github.com/sstent/go-garth/internal/utils" ) // GetOAuth1Token retrieves an OAuth1 token using the provided ticket diff --git a/internal/auth/sso/sso.go b/internal/auth/sso/sso.go index 66d6c8a..bcd8ffc 100644 --- a/internal/auth/sso/sso.go +++ b/internal/auth/sso/sso.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "go-garth/internal/auth/oauth" - types "go-garth/internal/models/types" + "github.com/sstent/go-garth/internal/auth/oauth" + types "github.com/sstent/go-garth/internal/models/types" ) var ( diff --git a/internal/data/base_test.go b/internal/data/base_test.go index 01b02f4..c706e43 100644 --- a/internal/data/base_test.go +++ b/internal/data/base_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/api/client" "github.com/stretchr/testify/assert" ) diff --git a/internal/data/body_battery.go b/internal/data/body_battery.go index fbd8efd..e2060d4 100644 --- a/internal/data/body_battery.go +++ b/internal/data/body_battery.go @@ -6,8 +6,8 @@ import ( "sort" "time" - types "go-garth/internal/models/types" - shared "go-garth/shared/interfaces" + types "github.com/sstent/go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" ) // BodyBatteryReading represents a single body battery data point diff --git a/internal/data/body_battery_test.go b/internal/data/body_battery_test.go index 807daf3..07733c3 100644 --- a/internal/data/body_battery_test.go +++ b/internal/data/body_battery_test.go @@ -1,7 +1,7 @@ package data import ( - types "go-garth/internal/models/types" + types "github.com/sstent/go-garth/internal/models/types" "testing" "github.com/stretchr/testify/assert" diff --git a/internal/data/hrv.go b/internal/data/hrv.go index aad98d3..b9ea79b 100644 --- a/internal/data/hrv.go +++ b/internal/data/hrv.go @@ -6,8 +6,8 @@ import ( "sort" "time" - types "go-garth/internal/models/types" - shared "go-garth/shared/interfaces" + types "github.com/sstent/go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" ) // DailyHRVDataWithMethods embeds types.DailyHRVData and adds methods diff --git a/internal/data/sleep.go b/internal/data/sleep.go index 386e7a2..99ecd33 100644 --- a/internal/data/sleep.go +++ b/internal/data/sleep.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - shared "go-garth/shared/interfaces" - types "go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" + types "github.com/sstent/go-garth/internal/models/types" ) // DailySleepDTO represents daily sleep data diff --git a/internal/data/sleep_detailed.go b/internal/data/sleep_detailed.go index 9a11941..6a6d4a6 100644 --- a/internal/data/sleep_detailed.go +++ b/internal/data/sleep_detailed.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - types "go-garth/internal/models/types" - shared "go-garth/shared/interfaces" + types "github.com/sstent/go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" ) // DetailedSleepDataWithMethods embeds types.DetailedSleepData and adds methods diff --git a/internal/data/training.go b/internal/data/training.go index 03bad51..76f02ea 100644 --- a/internal/data/training.go +++ b/internal/data/training.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - types "go-garth/internal/models/types" - shared "go-garth/shared/interfaces" + types "github.com/sstent/go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" ) // TrainingStatusWithMethods embeds types.TrainingStatus and adds methods diff --git a/internal/data/vo2max.go b/internal/data/vo2max.go index 1c31be1..06afb78 100644 --- a/internal/data/vo2max.go +++ b/internal/data/vo2max.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - shared "go-garth/shared/interfaces" - types "go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" + types "github.com/sstent/go-garth/internal/models/types" ) // VO2MaxData implements the Data interface for VO2 max retrieval diff --git a/internal/data/vo2max_test.go b/internal/data/vo2max_test.go index 6060cb1..6f6a7de 100644 --- a/internal/data/vo2max_test.go +++ b/internal/data/vo2max_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "go-garth/internal/api/client" - "go-garth/internal/models" + types "github.com/sstent/go-garth/internal/models/types" + "github.com/sstent/go-garth/shared/interfaces" + "github.com/sstent/go-garth/shared/models" "github.com/stretchr/testify/assert" ) @@ -14,9 +15,9 @@ func TestVO2MaxData_Get(t *testing.T) { // Setup runningVO2 := 45.0 cyclingVO2 := 50.0 - settings := &client.UserSettings{ + settings := &models.UserSettings{ ID: 12345, - UserData: client.UserData{ + UserData: models.UserData{ VO2MaxRunning: &runningVO2, VO2MaxCycling: &cyclingVO2, }, @@ -25,14 +26,14 @@ func TestVO2MaxData_Get(t *testing.T) { vo2Data := NewVO2MaxData() // Mock the get function - vo2Data.GetFunc = func(day time.Time, c *client.Client) (interface{}, error) { - vo2Profile := &models.VO2MaxProfile{ + vo2Data.GetFunc = func(day time.Time, c interfaces.APIClient) (interface{}, error) { + vo2Profile := &types.VO2MaxProfile{ UserProfilePK: settings.ID, LastUpdated: time.Now(), } if settings.UserData.VO2MaxRunning != nil && *settings.UserData.VO2MaxRunning > 0 { - vo2Profile.Running = &models.VO2MaxEntry{ + vo2Profile.Running = &types.VO2MaxEntry{ Value: *settings.UserData.VO2MaxRunning, ActivityType: "running", Date: day, @@ -41,7 +42,7 @@ func TestVO2MaxData_Get(t *testing.T) { } if settings.UserData.VO2MaxCycling != nil && *settings.UserData.VO2MaxCycling > 0 { - vo2Profile.Cycling = &models.VO2MaxEntry{ + vo2Profile.Cycling = &types.VO2MaxEntry{ Value: *settings.UserData.VO2MaxCycling, ActivityType: "cycling", Date: day, @@ -58,7 +59,7 @@ func TestVO2MaxData_Get(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, result) - profile, ok := result.(*models.VO2MaxProfile) + profile, ok := result.(*types.VO2MaxProfile) assert.True(t, ok) assert.Equal(t, 12345, profile.UserProfilePK) assert.NotNil(t, profile.Running) diff --git a/internal/data/weight.go b/internal/data/weight.go index ca95454..4098944 100644 --- a/internal/data/weight.go +++ b/internal/data/weight.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - shared "go-garth/shared/interfaces" + shared "github.com/sstent/go-garth/shared/interfaces" ) // WeightData represents weight data diff --git a/internal/models/types/types.go b/internal/models/types/types.go new file mode 100644 index 0000000..560de7e --- /dev/null +++ b/internal/models/types/types.go @@ -0,0 +1,469 @@ +package types + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// TokenRefresher is an interface for refreshing a token. +type TokenRefresher interface { + RefreshSession() error +} + +// OAuthConsumer represents OAuth consumer credentials +type OAuthConsumer struct { + ConsumerKey string `json:"consumer_key"` + ConsumerSecret string `json:"consumer_secret"` +} + +// OAuth1Token represents OAuth1 token response +type OAuth1Token struct { + OAuthToken string `json:"oauth_token"` + OAuthTokenSecret string `json:"oauth_token_secret"` + MFAToken string `json:"mfa_token,omitempty"` + Domain string `json:"domain"` +} + +// OAuth2Token represents OAuth2 token response +type OAuth2Token struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + ExpiresAt time.Time `json:"expires_at"` +} + +// SessionData represents the data stored in a session file +type SessionData struct { + Domain string `json:"domain"` + Username string `json:"username"` + AuthToken string `json:"auth_token"` + OAuth2Token *OAuth2Token `json:"oauth2_token"` +} + +// Expired checks if token is expired +func (t *OAuth2Token) Expired() bool { + return time.Now().After(t.ExpiresAt) +} + +// RefreshIfNeeded refreshes token if expired +func (t *OAuth2Token) RefreshIfNeeded(client TokenRefresher) error { + if !t.Expired() { + return nil + } + + return client.RefreshSession() +} + +var ( + // Default location for conversions (set to UTC by default) + defaultLocation *time.Location +) + +func init() { + var err error + defaultLocation, err = time.LoadLocation("UTC") + if err != nil { + panic(err) + } +} + +// ParseTimestamp converts a millisecond timestamp to time.Time in default location +func ParseTimestamp(ts int) time.Time { + return time.Unix(0, int64(ts)*int64(time.Millisecond)).In(defaultLocation) +} + +// parseAggregationKey is a helper function to parse aggregation key back to a time.Time object +func ParseAggregationKey(key, aggregate string) time.Time { + switch aggregate { + case "day": + t, _ := time.Parse("2006-01-02", key) + return t + case "week": + year, _ := strconv.Atoi(key[:4]) + week, _ := strconv.Atoi(key[6:]) + t := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) + // Find the first Monday of the year + for t.Weekday() != time.Monday { + t = t.AddDate(0, 0, 1) + } + // Add weeks + return t.AddDate(0, 0, (week-1)*7) + case "month": + t, _ := time.Parse("2006-01", key) + return t + case "year": + t, _ := time.Parse("2006", key) + return t + } + return time.Time{} +} + +// GarminTime represents Garmin's timestamp format with custom JSON parsing +type GarminTime struct { + time.Time +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It parses Garmin's specific timestamp format. +func (gt *GarminTime) UnmarshalJSON(b []byte) (err error) { + s := strings.Trim(string(b), `"`) + if s == "null" { + return nil + } + + // Try parsing with milliseconds (e.g., "2018-09-01T00:13:25.000") + // Garmin sometimes returns .0 for milliseconds, which Go's time.Parse handles as .000 + // The 'Z' in the layout indicates a UTC time without a specific offset, which is often how these are interpreted. + // If the input string does not contain 'Z', it will be parsed as local time. + // For consistency, we'll assume UTC if no timezone is specified. + layouts := []string{ + "2006-01-02T15:04:05.0", // Example: 2018-09-01T00:13:25.0 + "2006-01-02T15:04:05", // Example: 2018-09-01T00:13:25 + "2006-01-02 15:04:05", // Example: 2025-09-18 15:14:40 + "2006-01-02", // Example: 2018-09-01 + } + + for _, layout := range layouts { + if t, err := time.Parse(layout, s); err == nil { + gt.Time = t + return nil + } + } + + return fmt.Errorf("cannot parse %q into a GarminTime", s) +} + + +// ActivityType represents the type of activity +type ActivityType struct { + TypeID int `json:"typeId"` + TypeKey string `json:"typeKey"` + ParentTypeID *int `json:"parentTypeId,omitempty"` +} + +// EventType represents the event type of an activity +type EventType struct { + TypeID int `json:"typeId"` + TypeKey string `json:"typeKey"` +} + +// Activity represents a Garmin Connect activity +type Activity struct { + ActivityID int64 `json:"activityId"` + ActivityName string `json:"activityName"` + Description string `json:"description"` + StartTimeLocal GarminTime `json:"startTimeLocal"` + StartTimeGMT GarminTime `json:"startTimeGMT"` + ActivityType ActivityType `json:"activityType"` + EventType EventType `json:"eventType"` + Distance float64 `json:"distance"` + Duration float64 `json:"duration"` + ElapsedDuration float64 `json:"elapsedDuration"` + MovingDuration float64 `json:"movingDuration"` + ElevationGain float64 `json:"elevationGain"` + ElevationLoss float64 `json:"elevationLoss"` + AverageSpeed float64 `json:"averageSpeed"` + MaxSpeed float64 `json:"maxSpeed"` + Calories float64 `json:"calories"` + AverageHR float64 `json:"averageHR"` + MaxHR float64 `json:"maxHR"` +} + +// UserProfile represents a Garmin user profile +type UserProfile struct { + UserName string `json:"userName"` + DisplayName string `json:"displayName"` + LevelUpdateDate GarminTime `json:"levelUpdateDate"` + // Add other fields as needed from API response +} + +// VO2MaxData represents VO2 max data +type VO2MaxData struct { + Date time.Time `json:"calendarDate"` + VO2MaxRunning *float64 `json:"vo2MaxRunning"` + VO2MaxCycling *float64 `json:"vo2MaxCycling"` + UserProfilePK int `json:"userProfilePk"` +} + +// Add these new structs +type VO2MaxEntry struct { + Value float64 `json:"value"` + ActivityType string `json:"activityType"` // "running" or "cycling" + Date time.Time `json:"date"` + Source string `json:"source"` // "user_settings", "activity", etc. +} + +type VO2Max struct { + Value float64 `json:"vo2Max"` + FitnessLevel string `json:"fitnessLevel"` + 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"` + 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"` +} + +// HRVBaseline represents HRV baseline data +type HRVBaseline struct { + LowUpper int `json:"lowUpper"` + 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"` +} + +// 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"` +} + +// 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"` +} + +// 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"` +} + +// 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"` +} + +// HeartRateZones represents heart rate zone data +type HeartRateZones struct { + RestingHR int `json:"resting_hr"` + MaxHR int `json:"max_hr"` + LactateThreshold int `json:"lactate_threshold"` + Zones []HRZone `json:"zones"` + UpdatedAt time.Time `json:"updated_at"` +} + +// HRZone represents a single heart rate zone +type HRZone struct { + Zone int `json:"zone"` + MinBPM int `json:"min_bpm"` + MaxBPM int `json:"max_bpm"` + Name string `json:"name"` +} + +// WellnessData represents additional wellness metrics +type WellnessData struct { + Date time.Time `json:"calendarDate"` + RestingHR *int `json:"resting_hr"` + Weight *float64 `json:"weight"` + BodyFat *float64 `json:"body_fat"` + BMI *float64 `json:"bmi"` + BodyWater *float64 `json:"body_water"` + BoneMass *float64 `json:"bone_mass"` + MuscleMass *float64 `json:"muscle_mass"` + // Add more fields as needed +} + +// SleepData represents sleep summary data +type SleepData struct { + Date time.Time `json:"calendarDate"` + SleepScore int `json:"sleepScore"` + TotalSleepSeconds int `json:"totalSleepSeconds"` + DeepSleepSeconds int `json:"deepSleepSeconds"` + LightSleepSeconds int `json:"lightSleepSeconds"` + RemSleepSeconds int `json:"remSleepSeconds"` + AwakeSleepSeconds int `json:"awakeSleepSeconds"` + // Add more fields as needed +} + +// HrvData represents Heart Rate Variability data +type HrvData struct { + Date time.Time `json:"calendarDate"` + HrvValue float64 `json:"hrvValue"` + // Add more fields as needed +} + +// 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"` +} + +// HRVReading represents an individual HRV reading +type HRVReading struct { + Timestamp int `json:"timestamp"` + StressLevel int `json:"stressLevel"` + HeartRate int `json:"heartRate"` + RRInterval int `json:"rrInterval"` + Status string `json:"status"` + SignalQuality float64 `json:"signalQuality"` +} + +// TimestampAsTime converts the reading timestamp to time.Time using timeutils +func (r *HRVReading) TimestampAsTime() time.Time { + return ParseTimestamp(r.Timestamp) +} + +// RRSeconds converts the RR interval to seconds +func (r *HRVReading) RRSeconds() float64 { + return float64(r.RRInterval) / 1000.0 +} + +// StressData represents stress level data +type StressData struct { + Date time.Time `json:"calendarDate"` + StressLevel int `json:"stressLevel"` + RestStressLevel int `json:"restStressLevel"` + // Add more fields as needed +} + +// BodyBatteryData represents Body Battery data +type BodyBatteryData struct { + Date time.Time `json:"calendarDate"` + BatteryLevel int `json:"batteryLevel"` + Charge int `json:"charge"` + Drain int `json:"drain"` + // Add more fields as needed +} + +// StepsData represents steps statistics +type StepsData struct { + Date time.Time `json:"calendarDate"` + Steps int `json:"steps"` +} + +// DistanceData represents distance statistics +type DistanceData struct { + Date time.Time `json:"calendarDate"` + Distance float64 `json:"distance"` // in meters +} + +// CaloriesData represents calories statistics +type CaloriesData struct { + Date time.Time `json:"calendarDate"` + Calories int `json:"activeCalories"` +} diff --git a/internal/stats/base.go b/internal/stats/base.go index ff1f856..ca65604 100644 --- a/internal/stats/base.go +++ b/internal/stats/base.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "go-garth/internal/api/client" - "go-garth/internal/utils" + "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/utils" ) type Stats interface { diff --git a/internal/testutils/mock_client.go b/internal/testutils/mock_client.go index e64e348..1a15791 100644 --- a/internal/testutils/mock_client.go +++ b/internal/testutils/mock_client.go @@ -5,7 +5,7 @@ import ( "io" "net/url" - "go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/api/client" ) // MockClient simulates API client for tests diff --git a/internal/users/settings.go b/internal/users/settings.go index ba58822..4efa9cf 100644 --- a/internal/users/settings.go +++ b/internal/users/settings.go @@ -3,7 +3,7 @@ package users import ( "time" - "go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/api/client" ) type PowerFormat struct { diff --git a/main.go b/main.go index c62e98a..6690b3a 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,9 @@ import ( "log" "time" - "go-garth/internal/api/client" - "go-garth/internal/auth/credentials" - types "go-garth/pkg/garmin" + "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/auth/credentials" + types "github.com/sstent/go-garth/pkg/garmin" ) func main() { diff --git a/pkg/garmin/benchmark_test.go b/pkg/garmin/benchmark_test.go index e4abc7b..4a53931 100644 --- a/pkg/garmin/benchmark_test.go +++ b/pkg/garmin/benchmark_test.go @@ -2,9 +2,9 @@ package garmin_test import ( "encoding/json" - "go-garth/internal/api/client" - "go-garth/internal/data" - "go-garth/internal/testutils" + "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/data" + "github.com/sstent/go-garth/internal/testutils" "testing" "time" ) diff --git a/pkg/garmin/client.go b/pkg/garmin/client.go index 6de74cb..850e0b9 100644 --- a/pkg/garmin/client.go +++ b/pkg/garmin/client.go @@ -8,11 +8,11 @@ import ( "path/filepath" "time" - internalClient "go-garth/internal/api/client" - "go-garth/internal/errors" - types "go-garth/internal/models/types" - shared "go-garth/shared/interfaces" - models "go-garth/shared/models" + internalClient "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/errors" + types "github.com/sstent/go-garth/internal/models/types" + shared "github.com/sstent/go-garth/shared/interfaces" + models "github.com/sstent/go-garth/shared/models" ) // Client is the main Garmin Connect client type diff --git a/pkg/garmin/health.go b/pkg/garmin/health.go index c9a8a82..78cc41a 100644 --- a/pkg/garmin/health.go +++ b/pkg/garmin/health.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - internalClient "go-garth/internal/api/client" - "go-garth/internal/models/types" + internalClient "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/models/types" ) func (c *Client) GetDailyHRVData(date time.Time) (*types.DailyHRVData, error) { diff --git a/pkg/garmin/integration_test.go b/pkg/garmin/integration_test.go index 5462ee6..9c89bbf 100644 --- a/pkg/garmin/integration_test.go +++ b/pkg/garmin/integration_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "go-garth/internal/api/client" - "go-garth/internal/data" - "go-garth/internal/stats" + "github.com/sstent/go-garth/internal/api/client" + "github.com/sstent/go-garth/internal/data" + "github.com/sstent/go-garth/internal/stats" ) func TestBodyBatteryIntegration(t *testing.T) { diff --git a/pkg/garmin/stats.go b/pkg/garmin/stats.go index 9683f71..5017b4f 100644 --- a/pkg/garmin/stats.go +++ b/pkg/garmin/stats.go @@ -3,7 +3,7 @@ package garmin import ( "time" - "go-garth/internal/stats" + "github.com/sstent/go-garth/internal/stats" ) // Stats is an interface for stats data types. diff --git a/pkg/garmin/types.go b/pkg/garmin/types.go index a6768bd..e6bac24 100644 --- a/pkg/garmin/types.go +++ b/pkg/garmin/types.go @@ -1,6 +1,6 @@ package garmin -import types "go-garth/internal/models/types" +import types "github.com/sstent/go-garth/internal/models/types" // GarminTime represents Garmin's timestamp format with custom JSON parsing type GarminTime = types.GarminTime diff --git a/shared/interfaces/api_client.go b/shared/interfaces/api_client.go index cc5c652..ee7e4f5 100644 --- a/shared/interfaces/api_client.go +++ b/shared/interfaces/api_client.go @@ -5,8 +5,8 @@ import ( "net/url" "time" - types "go-garth/internal/models/types" - "go-garth/shared/models" + types "github.com/sstent/go-garth/internal/models/types" + "github.com/sstent/go-garth/shared/models" ) // APIClient defines the interface for making API calls that data packages need. diff --git a/shared/interfaces/data.go b/shared/interfaces/data.go index e45fbe1..8537f5f 100644 --- a/shared/interfaces/data.go +++ b/shared/interfaces/data.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "go-garth/internal/utils" + "github.com/sstent/go-garth/internal/utils" ) // Data defines the interface for Garmin Connect data models.