diff --git a/README.md b/README.md index 702dea3..0052ef5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # Garmin Connect Go Client +[![Go Reference](https://pkg.go.dev/badge/github.com/sstent/go-garth/pkg/garmin.svg)](https://pkg.go.dev/github.com/sstent/go-garth/pkg/garmin) + Go port of the Garth Python library for accessing Garmin Connect data. Provides full API coverage with improved performance and type safety. ## Installation ```bash -go get github.com/sstent/garmin-connect/garth +go get github.com/sstent/go-garth/pkg/garmin ``` ## Basic Usage @@ -14,12 +16,12 @@ package main import ( "fmt" "time" - "garmin-connect/garth" + "github.com/sstent/go-garth/pkg/garmin" ) func main() { // Create client and authenticate - client, err := garth.NewClient("garmin.com") + client, err := garmin.NewClient("garmin.com") if err != nil { panic(err) } @@ -29,9 +31,9 @@ func main() { panic(err) } - // Get yesterday's body battery data + // Get yesterday's body battery data (detailed) yesterday := time.Now().AddDate(0, 0, -1) - bb, err := garth.BodyBatteryData{}.Get(yesterday, client) + bb, err := client.GetBodyBatteryData(yesterday) if err != nil { panic(err) } @@ -41,16 +43,16 @@ func main() { } // Get weekly steps - steps := garth.NewDailySteps() + steps := garmin.NewDailySteps() stepData, err := steps.List(time.Now(), 7, client) if err != nil { panic(err) } for _, s := range stepData { - fmt.Printf("%s: %d steps\n", - s.(garth.DailySteps).CalendarDate.Format("2006-01-02"), - *s.(garth.DailySteps).TotalSteps) + fmt.Printf("%s: %d steps\n", + s.(garmin.DailySteps).CalendarDate.Format("2006-01-02"), + *s.(garmin.DailySteps).TotalSteps) } } ``` @@ -102,7 +104,7 @@ BenchmarkSleepList-8 50000 35124 ns/op (7 days) ``` ## Documentation -Full API docs: [https://pkg.go.dev/garmin-connect/garth](https://pkg.go.dev/garmin-connect/garth) +Full API docs: [https://pkg.go.dev/github.com/sstent/go-garth/pkg/garmin](https://pkg.go.dev/github.com/sstent/go-garth/pkg/garmin) ## CLI Tool Includes `cmd/garth` CLI for data export. Supports both daily and weekly stats: diff --git a/internal/api/client/doc.go b/internal/api/client/doc.go new file mode 100644 index 0000000..be09fbf --- /dev/null +++ b/internal/api/client/doc.go @@ -0,0 +1,6 @@ +// Package client implements the low-level Garmin Connect HTTP client. +// It is responsible for authentication (via SSO helpers), request construction, +// header and cookie handling, error mapping, and JSON decoding. Higher-level +// public APIs in pkg/garmin delegate to this package for actual network I/O. +// Note: This is an internal package and not intended for direct external use. +package client diff --git a/internal/auth/credentials/credentials.go b/internal/auth/credentials/credentials.go index df27b1a..70f1f01 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/github.com/sstent/go-garth" + projectRoot := "/home/sstent/Projects/go-garth" envPath := filepath.Join(projectRoot, ".env") // Load .env file @@ -34,4 +34,4 @@ func LoadEnvCredentials() (email, password, domain string, err error) { } return email, password, domain, nil -} \ No newline at end of file +} diff --git a/internal/auth/credentials/doc.go b/internal/auth/credentials/doc.go new file mode 100644 index 0000000..b84678d --- /dev/null +++ b/internal/auth/credentials/doc.go @@ -0,0 +1,4 @@ +// Package credentials provides helpers for loading user credentials and +// environment configuration used during authentication and local development. +// Note: This is an internal package and not intended for direct external use. +package credentials diff --git a/internal/auth/oauth/doc.go b/internal/auth/oauth/doc.go new file mode 100644 index 0000000..0f89792 --- /dev/null +++ b/internal/auth/oauth/doc.go @@ -0,0 +1,5 @@ +// Package oauth contains low-level OAuth1 and OAuth2 flows used by SSO to +// obtain and exchange tokens. It handles request signing, headers, and response +// parsing to produce strongly-typed token structures for the client. +// Note: This is an internal package and not intended for direct external use. +package oauth diff --git a/internal/auth/sso/doc.go b/internal/auth/sso/doc.go new file mode 100644 index 0000000..bd5308b --- /dev/null +++ b/internal/auth/sso/doc.go @@ -0,0 +1,5 @@ +// Package sso implements the Garmin SSO login flow. It orchestrates CSRF, +// ticket exchange, MFA placeholders, and token retrieval, delegating OAuth +// details to internal/auth/oauth. The internal client consumes this package. +// Note: This is an internal package and not intended for direct external use. +package sso diff --git a/internal/config/doc.go b/internal/config/doc.go new file mode 100644 index 0000000..afae927 --- /dev/null +++ b/internal/config/doc.go @@ -0,0 +1,5 @@ +// Package config defines the application configuration schema and helpers for +// locating, loading, saving, and initializing configuration files following +// conventional XDG directory layout. +// Note: This is an internal package and not intended for direct external use. +package config diff --git a/internal/data/doc.go b/internal/data/doc.go new file mode 100644 index 0000000..f05620d --- /dev/null +++ b/internal/data/doc.go @@ -0,0 +1,5 @@ +// Package data provides helpers and enrichments for Garmin wellness and metric +// data. It includes parsing utilities, convenience wrappers that add methods to +// decoded responses, and transformations used by higher-level APIs. +// Note: This is an internal package and not intended for direct external use. +package data diff --git a/internal/errors/doc.go b/internal/errors/doc.go new file mode 100644 index 0000000..e3629c9 --- /dev/null +++ b/internal/errors/doc.go @@ -0,0 +1,5 @@ +// Package errors defines structured error types used across the module, +// including APIError, IOError, AuthenticationError, OAuthError, and +// ValidationError. These implement error wrapping and preserve HTTP context. +// Note: This is an internal package and not intended for direct external use. +package errors diff --git a/internal/models/types/doc.go b/internal/models/types/doc.go new file mode 100644 index 0000000..565b34b --- /dev/null +++ b/internal/models/types/doc.go @@ -0,0 +1,5 @@ +// Package types defines core domain models mapped to Garmin Connect API JSON. +// It includes user profile, wellness metrics, sleep detail, HRV, body battery, +// training status/load, time helpers, and related structures. +// Note: This is an internal package and not intended for direct external use. +package types diff --git a/internal/models/types/garmin.go b/internal/models/types/garmin.go index de7d785..237ea52 100644 --- a/internal/models/types/garmin.go +++ b/internal/models/types/garmin.go @@ -70,6 +70,7 @@ func (gt *GarminTime) UnmarshalJSON(b []byte) (err error) { // 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-02 15:04:05", // Example: 2025-09-21 07:18:03 "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", // Example: 2018-09-01 diff --git a/internal/models/types/garmin_test.go b/internal/models/types/garmin_test.go new file mode 100644 index 0000000..62e066f --- /dev/null +++ b/internal/models/types/garmin_test.go @@ -0,0 +1,82 @@ +package types + +import ( + "encoding/json" + "testing" + "time" +) + +func TestGarminTime_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expected time.Time + wantErr bool + }{ + { + name: "space separated format", + input: `"2025-09-21 07:18:03"`, + expected: time.Date(2025, 9, 21, 7, 18, 3, 0, time.UTC), + wantErr: false, + }, + { + name: "T separator with milliseconds", + input: `"2018-09-01T00:13:25.0"`, + expected: time.Date(2018, 9, 1, 0, 13, 25, 0, time.UTC), + wantErr: false, + }, + { + name: "T separator without milliseconds", + input: `"2018-09-01T00:13:25"`, + expected: time.Date(2018, 9, 1, 0, 13, 25, 0, time.UTC), + wantErr: false, + }, + { + name: "date only", + input: `"2018-09-01"`, + expected: time.Date(2018, 9, 1, 0, 0, 0, 0, time.UTC), + wantErr: false, + }, + { + name: "invalid format", + input: `"invalid"`, + wantErr: true, + }, + { + name: "null value", + input: "null", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gt GarminTime + err := json.Unmarshal([]byte(tt.input), >) + + if tt.wantErr { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if tt.input == "null" { + // For null values, the time should be zero + if !gt.Time.IsZero() { + t.Errorf("expected zero time for null input, got %v", gt.Time) + } + return + } + + if !gt.Time.Equal(tt.expected) { + t.Errorf("expected %v, got %v", tt.expected, gt.Time) + } + }) + } +} diff --git a/internal/stats/doc.go b/internal/stats/doc.go new file mode 100644 index 0000000..f37070b --- /dev/null +++ b/internal/stats/doc.go @@ -0,0 +1,5 @@ +// Package stats provides typed accessors for aggregated statistics endpoints, +// including daily and weekly variants for steps, stress, hydration, sleep, HRV, +// and intensity minutes. Pagination and date-window logic live here. +// Note: This is an internal package and not intended for direct external use. +package stats diff --git a/internal/testutils/doc.go b/internal/testutils/doc.go new file mode 100644 index 0000000..af21121 --- /dev/null +++ b/internal/testutils/doc.go @@ -0,0 +1,4 @@ +// Package testutils contains helpers for tests such as HTTP servers and mock +// clients. It is used by unit and integration tests within this repository. +// Note: This is an internal package and not intended for direct external use. +package testutils diff --git a/internal/users/doc.go b/internal/users/doc.go new file mode 100644 index 0000000..26b6c2f --- /dev/null +++ b/internal/users/doc.go @@ -0,0 +1,4 @@ +// Package users contains structures related to user settings and sleep windows. +// It mirrors selected Garmin user profile payloads used in feature logic. +// Note: This is an internal package and not intended for direct external use. +package users diff --git a/internal/utils/doc.go b/internal/utils/doc.go new file mode 100644 index 0000000..d01ec63 --- /dev/null +++ b/internal/utils/doc.go @@ -0,0 +1,4 @@ +// Package utils provides general helpers such as OAuth signing utilities, +// time conversions, date range helpers, and string case conversions. +// Note: This is an internal package and not intended for direct external use. +package utils diff --git a/pkg/auth/oauth/doc.go b/pkg/auth/oauth/doc.go new file mode 100644 index 0000000..3ab3abc --- /dev/null +++ b/pkg/auth/oauth/doc.go @@ -0,0 +1,6 @@ +// Package oauth provides public wrappers around the internal OAuth helpers. +// It exposes functions to obtain OAuth1 tokens and exchange them for OAuth2 +// tokens compatible with the public garmin package types. External consumers +// should use this package when they need token bootstrapping independent of +// a fully initialized client. +package oauth diff --git a/pkg/garmin/health.go b/pkg/garmin/health.go index 78cc41a..293fad4 100644 --- a/pkg/garmin/health.go +++ b/pkg/garmin/health.go @@ -9,6 +9,8 @@ import ( "github.com/sstent/go-garth/internal/models/types" ) +// GetDailyHRVData retrieves comprehensive daily HRV data for the given date. +// It returns nil when no HRV data is available for the specified day. func (c *Client) GetDailyHRVData(date time.Time) (*types.DailyHRVData, error) { return getDailyHRVData(date, c.Client) } @@ -41,6 +43,9 @@ func getDailyHRVData(day time.Time, client *internalClient.Client) (*types.Daily return &response.HRVSummary, nil } +// GetDetailedSleepData retrieves comprehensive sleep data for the given date, +// including sleep stages and movement where available. It returns nil when no +// sleep data is available for the specified day. func (c *Client) GetDetailedSleepData(date time.Time) (*types.DetailedSleepData, error) { return getDetailedSleepData(date, c.Client) } diff --git a/shared/interfaces/doc.go b/shared/interfaces/doc.go new file mode 100644 index 0000000..b62a0f7 --- /dev/null +++ b/shared/interfaces/doc.go @@ -0,0 +1,4 @@ +// Package interfaces defines narrow contracts shared across packages. +// Notably, APIClient abstracts HTTP access required by data and stats layers, +// and BaseData/Data define the concurrent day-by-day retrieval pattern. +package interfaces diff --git a/shared/models/doc.go b/shared/models/doc.go new file mode 100644 index 0000000..05d2e70 --- /dev/null +++ b/shared/models/doc.go @@ -0,0 +1,4 @@ +// Package models defines shared data models that are safe to expose to public +// packages, including user settings and related sub-structures consumed by +// both internal client code and public wrappers. +package models