mirror of
https://github.com/sstent/go-garth.git
synced 2025-12-06 08:01:42 +00:00
fixed timecode issue
This commit is contained in:
22
README.md
22
README.md
@@ -1,10 +1,12 @@
|
|||||||
# Garmin Connect Go Client
|
# Garmin Connect Go Client
|
||||||
|
|
||||||
|
[](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.
|
Go port of the Garth Python library for accessing Garmin Connect data. Provides full API coverage with improved performance and type safety.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
```bash
|
```bash
|
||||||
go get github.com/sstent/garmin-connect/garth
|
go get github.com/sstent/go-garth/pkg/garmin
|
||||||
```
|
```
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
@@ -14,12 +16,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
"garmin-connect/garth"
|
"github.com/sstent/go-garth/pkg/garmin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create client and authenticate
|
// Create client and authenticate
|
||||||
client, err := garth.NewClient("garmin.com")
|
client, err := garmin.NewClient("garmin.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -29,9 +31,9 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get yesterday's body battery data
|
// Get yesterday's body battery data (detailed)
|
||||||
yesterday := time.Now().AddDate(0, 0, -1)
|
yesterday := time.Now().AddDate(0, 0, -1)
|
||||||
bb, err := garth.BodyBatteryData{}.Get(yesterday, client)
|
bb, err := client.GetBodyBatteryData(yesterday)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -41,16 +43,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get weekly steps
|
// Get weekly steps
|
||||||
steps := garth.NewDailySteps()
|
steps := garmin.NewDailySteps()
|
||||||
stepData, err := steps.List(time.Now(), 7, client)
|
stepData, err := steps.List(time.Now(), 7, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range stepData {
|
for _, s := range stepData {
|
||||||
fmt.Printf("%s: %d steps\n",
|
fmt.Printf("%s: %d steps\n",
|
||||||
s.(garth.DailySteps).CalendarDate.Format("2006-01-02"),
|
s.(garmin.DailySteps).CalendarDate.Format("2006-01-02"),
|
||||||
*s.(garth.DailySteps).TotalSteps)
|
*s.(garmin.DailySteps).TotalSteps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -102,7 +104,7 @@ BenchmarkSleepList-8 50000 35124 ns/op (7 days)
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## 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
|
## CLI Tool
|
||||||
Includes `cmd/garth` CLI for data export. Supports both daily and weekly stats:
|
Includes `cmd/garth` CLI for data export. Supports both daily and weekly stats:
|
||||||
|
|||||||
6
internal/api/client/doc.go
Normal file
6
internal/api/client/doc.go
Normal file
@@ -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
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
// LoadEnvCredentials loads credentials from .env file
|
// LoadEnvCredentials loads credentials from .env file
|
||||||
func LoadEnvCredentials() (email, password, domain string, err error) {
|
func LoadEnvCredentials() (email, password, domain string, err error) {
|
||||||
// Determine project root (assuming .env is in the project root)
|
// 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")
|
envPath := filepath.Join(projectRoot, ".env")
|
||||||
|
|
||||||
// Load .env file
|
// Load .env file
|
||||||
@@ -34,4 +34,4 @@ func LoadEnvCredentials() (email, password, domain string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return email, password, domain, nil
|
return email, password, domain, nil
|
||||||
}
|
}
|
||||||
|
|||||||
4
internal/auth/credentials/doc.go
Normal file
4
internal/auth/credentials/doc.go
Normal file
@@ -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
|
||||||
5
internal/auth/oauth/doc.go
Normal file
5
internal/auth/oauth/doc.go
Normal file
@@ -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
|
||||||
5
internal/auth/sso/doc.go
Normal file
5
internal/auth/sso/doc.go
Normal file
@@ -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
|
||||||
5
internal/config/doc.go
Normal file
5
internal/config/doc.go
Normal file
@@ -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
|
||||||
5
internal/data/doc.go
Normal file
5
internal/data/doc.go
Normal file
@@ -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
|
||||||
5
internal/errors/doc.go
Normal file
5
internal/errors/doc.go
Normal file
@@ -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
|
||||||
5
internal/models/types/doc.go
Normal file
5
internal/models/types/doc.go
Normal file
@@ -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
|
||||||
@@ -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.
|
// 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.
|
// For consistency, we'll assume UTC if no timezone is specified.
|
||||||
layouts := []string{
|
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.0", // Example: 2018-09-01T00:13:25.0
|
||||||
"2006-01-02T15:04:05", // Example: 2018-09-01T00:13:25
|
"2006-01-02T15:04:05", // Example: 2018-09-01T00:13:25
|
||||||
"2006-01-02", // Example: 2018-09-01
|
"2006-01-02", // Example: 2018-09-01
|
||||||
|
|||||||
82
internal/models/types/garmin_test.go
Normal file
82
internal/models/types/garmin_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
internal/stats/doc.go
Normal file
5
internal/stats/doc.go
Normal file
@@ -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
|
||||||
4
internal/testutils/doc.go
Normal file
4
internal/testutils/doc.go
Normal file
@@ -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
|
||||||
4
internal/users/doc.go
Normal file
4
internal/users/doc.go
Normal file
@@ -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
|
||||||
4
internal/utils/doc.go
Normal file
4
internal/utils/doc.go
Normal file
@@ -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
|
||||||
6
pkg/auth/oauth/doc.go
Normal file
6
pkg/auth/oauth/doc.go
Normal file
@@ -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
|
||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/sstent/go-garth/internal/models/types"
|
"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) {
|
func (c *Client) GetDailyHRVData(date time.Time) (*types.DailyHRVData, error) {
|
||||||
return getDailyHRVData(date, c.Client)
|
return getDailyHRVData(date, c.Client)
|
||||||
}
|
}
|
||||||
@@ -41,6 +43,9 @@ func getDailyHRVData(day time.Time, client *internalClient.Client) (*types.Daily
|
|||||||
return &response.HRVSummary, nil
|
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) {
|
func (c *Client) GetDetailedSleepData(date time.Time) (*types.DetailedSleepData, error) {
|
||||||
return getDetailedSleepData(date, c.Client)
|
return getDetailedSleepData(date, c.Client)
|
||||||
}
|
}
|
||||||
|
|||||||
4
shared/interfaces/doc.go
Normal file
4
shared/interfaces/doc.go
Normal file
@@ -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
|
||||||
4
shared/models/doc.go
Normal file
4
shared/models/doc.go
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user