fixed timecode issue

This commit is contained in:
2025-09-22 10:53:41 -07:00
parent 4aa72fcd11
commit f2256a9cfe
20 changed files with 173 additions and 12 deletions

View File

@@ -1,10 +1,12 @@
# Garmin Connect Go Client # 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. 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:

View 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

View File

@@ -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
} }

View 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

View 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
View 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
View 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
View 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
View 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

View 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

View File

@@ -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

View 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), &gt)
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
View 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

View 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
View 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
View 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
View 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

View File

@@ -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
View 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
View 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