mirror of
https://github.com/sstent/go-garth.git
synced 2026-02-07 15:01:48 +00:00
porting - part2 wk2 done
This commit is contained in:
30
garth/__init__.go
Normal file
30
garth/__init__.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package garth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"garmin-connect/garth/client"
|
||||||
|
"garmin-connect/garth/data"
|
||||||
|
"garmin-connect/garth/stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Re-export main types for convenience
|
||||||
|
type Client = client.Client
|
||||||
|
|
||||||
|
// Data types
|
||||||
|
type BodyBatteryData = data.DailyBodyBatteryStress
|
||||||
|
type HRVData = data.HRVData
|
||||||
|
type SleepData = data.DailySleepDTO
|
||||||
|
type WeightData = data.WeightData
|
||||||
|
|
||||||
|
// Stats types
|
||||||
|
type DailySteps = stats.DailySteps
|
||||||
|
type DailyStress = stats.DailyStress
|
||||||
|
type DailyHRV = stats.DailyHRV
|
||||||
|
type DailyHydration = stats.DailyHydration
|
||||||
|
type DailyIntensityMinutes = stats.DailyIntensityMinutes
|
||||||
|
type DailySleep = stats.DailySleep
|
||||||
|
|
||||||
|
// Main functions
|
||||||
|
var (
|
||||||
|
NewClient = client.NewClient
|
||||||
|
Login = client.Login
|
||||||
|
)
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"garmin-connect/garth/client"
|
"garmin-connect/garth/client"
|
||||||
|
"garmin-connect/garth/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data defines the interface for Garmin Connect data types.
|
// Data defines the interface for Garmin Connect data types.
|
||||||
@@ -70,30 +71,27 @@ func (b *BaseData) Get(day time.Time, c *client.Client) (interface{}, error) {
|
|||||||
// []error: Slice of errors encountered during processing
|
// []error: Slice of errors encountered during processing
|
||||||
func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers int) ([]interface{}, []error) {
|
func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers int) ([]interface{}, []error) {
|
||||||
if maxWorkers < 1 {
|
if maxWorkers < 1 {
|
||||||
maxWorkers = 1
|
maxWorkers = 10 // Match Python's MAX_WORKERS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate date range (end backwards for 'days' days)
|
dates := utils.DateRange(end, days)
|
||||||
dates := make([]time.Time, days)
|
|
||||||
for i := 0; i < days; i++ {
|
// Define result type for channel
|
||||||
dates[i] = end.AddDate(0, 0, -i)
|
type result struct {
|
||||||
|
data interface{}
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
workCh := make(chan time.Time, days)
|
workCh := make(chan time.Time, days)
|
||||||
resultsCh := make(chan interface{}, days)
|
resultsCh := make(chan result, days)
|
||||||
errCh := make(chan error, days)
|
|
||||||
|
|
||||||
// Worker function
|
// Worker function
|
||||||
worker := func() {
|
worker := func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for date := range workCh {
|
for date := range workCh {
|
||||||
result, err := b.Get(date, c)
|
data, err := b.Get(date, c)
|
||||||
if err != nil {
|
resultsCh <- result{data: data, err: err}
|
||||||
errCh <- err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resultsCh <- result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +101,7 @@ func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers in
|
|||||||
go worker()
|
go worker()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send work to channel
|
// Send work
|
||||||
go func() {
|
go func() {
|
||||||
for _, date := range dates {
|
for _, date := range dates {
|
||||||
workCh <- date
|
workCh <- date
|
||||||
@@ -111,32 +109,20 @@ func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers in
|
|||||||
close(workCh)
|
close(workCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Close channels when all workers finish
|
// Close results channel when workers are done
|
||||||
go func() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(resultsCh)
|
close(resultsCh)
|
||||||
close(errCh)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Collect results and errors
|
|
||||||
var results []interface{}
|
var results []interface{}
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
// Collect results until both channels are closed
|
for r := range resultsCh {
|
||||||
for resultsCh != nil || errCh != nil {
|
if r.err != nil {
|
||||||
select {
|
errs = append(errs, r.err)
|
||||||
case result, ok := <-resultsCh:
|
} else if r.data != nil {
|
||||||
if !ok {
|
results = append(results, r.data)
|
||||||
resultsCh = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results = append(results, result)
|
|
||||||
case err, ok := <-errCh:
|
|
||||||
if !ok {
|
|
||||||
errCh = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"garmin-connect/garth/client"
|
"garmin-connect/garth/client"
|
||||||
"garmin-connect/garth/data"
|
"garmin-connect/garth/data"
|
||||||
|
"garmin-connect/garth/stats"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBodyBatteryIntegration(t *testing.T) {
|
func TestBodyBatteryIntegration(t *testing.T) {
|
||||||
@@ -37,3 +38,57 @@ func TestBodyBatteryIntegration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStatsEndpoints(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := client.NewClient("garmin.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load test session
|
||||||
|
err = c.LoadSession("test_session.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("No test session available")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stat stats.Stats
|
||||||
|
}{
|
||||||
|
{"Steps", stats.NewDailySteps()},
|
||||||
|
{"Stress", stats.NewDailyStress()},
|
||||||
|
{"Hydration", stats.NewDailyHydration()},
|
||||||
|
{"IntensityMinutes", stats.NewDailyIntensityMinutes()},
|
||||||
|
{"Sleep", stats.NewDailySleep()},
|
||||||
|
{"HRV", stats.NewDailyHRV()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
end := time.Now().AddDate(0, 0, -1)
|
||||||
|
results, err := tt.stat.List(end, 1, c)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("List failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) == 0 {
|
||||||
|
t.Logf("No data returned for %s", tt.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation that we got some data
|
||||||
|
resultMap, ok := results[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected map for %s result, got %T", tt.name, results[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resultMap) == 0 {
|
||||||
|
t.Errorf("Empty result map for %s", tt.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
90
garth/stats/base.go
Normal file
90
garth/stats/base.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"garmin-connect/garth/client"
|
||||||
|
"garmin-connect/garth/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stats interface {
|
||||||
|
List(end time.Time, period int, client *client.Client) ([]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseStats struct {
|
||||||
|
Path string
|
||||||
|
PageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseStats) List(end time.Time, period int, client *client.Client) ([]interface{}, error) {
|
||||||
|
endDate := utils.FormatEndDate(end)
|
||||||
|
|
||||||
|
if period > b.PageSize {
|
||||||
|
// Handle pagination - get first page
|
||||||
|
page, err := b.fetchPage(endDate, b.PageSize, client)
|
||||||
|
if err != nil || len(page) == 0 {
|
||||||
|
return page, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get remaining pages recursively
|
||||||
|
remainingStart := endDate.AddDate(0, 0, -b.PageSize)
|
||||||
|
remainingPeriod := period - b.PageSize
|
||||||
|
remainingData, err := b.List(remainingStart, remainingPeriod, client)
|
||||||
|
if err != nil {
|
||||||
|
return page, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(remainingData, page...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.fetchPage(endDate, period, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseStats) fetchPage(end time.Time, period int, client *client.Client) ([]interface{}, error) {
|
||||||
|
var start time.Time
|
||||||
|
var path string
|
||||||
|
|
||||||
|
if strings.Contains(b.Path, "daily") {
|
||||||
|
start = end.AddDate(0, 0, -(period - 1))
|
||||||
|
path = strings.Replace(b.Path, "{start}", start.Format("2006-01-02"), 1)
|
||||||
|
path = strings.Replace(path, "{end}", end.Format("2006-01-02"), 1)
|
||||||
|
} else {
|
||||||
|
path = strings.Replace(b.Path, "{end}", end.Format("2006-01-02"), 1)
|
||||||
|
path = strings.Replace(path, "{period}", fmt.Sprintf("%d", period), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.ConnectAPI(path, "GET", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response == nil {
|
||||||
|
return []interface{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
responseSlice, ok := response.([]interface{})
|
||||||
|
if !ok || len(responseSlice) == 0 {
|
||||||
|
return []interface{}{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []interface{}
|
||||||
|
for _, item := range responseSlice {
|
||||||
|
itemMap := item.(map[string]interface{})
|
||||||
|
|
||||||
|
// Handle nested "values" structure
|
||||||
|
if values, exists := itemMap["values"]; exists {
|
||||||
|
valuesMap := values.(map[string]interface{})
|
||||||
|
for k, v := range valuesMap {
|
||||||
|
itemMap[k] = v
|
||||||
|
}
|
||||||
|
delete(itemMap, "values")
|
||||||
|
}
|
||||||
|
|
||||||
|
snakeItem := utils.CamelToSnakeDict(itemMap)
|
||||||
|
results = append(results, snakeItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
21
garth/stats/hrv.go
Normal file
21
garth/stats/hrv.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const BASE_HRV_PATH = "/usersummary-service/stats/hrv"
|
||||||
|
|
||||||
|
type DailyHRV struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
RestingHR *int `json:"resting_hr"`
|
||||||
|
HRV *int `json:"hrv"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDailyHRV() *DailyHRV {
|
||||||
|
return &DailyHRV{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_HRV_PATH + "/daily/{start}/{end}",
|
||||||
|
PageSize: 28,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
20
garth/stats/hydration.go
Normal file
20
garth/stats/hydration.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const BASE_HYDRATION_PATH = "/usersummary-service/stats/hydration"
|
||||||
|
|
||||||
|
type DailyHydration struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
TotalWaterML *int `json:"total_water_ml"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDailyHydration() *DailyHydration {
|
||||||
|
return &DailyHydration{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_HYDRATION_PATH + "/daily/{start}/{end}",
|
||||||
|
PageSize: 28,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
21
garth/stats/intensity_minutes.go
Normal file
21
garth/stats/intensity_minutes.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const BASE_INTENSITY_PATH = "/usersummary-service/stats/intensity_minutes"
|
||||||
|
|
||||||
|
type DailyIntensityMinutes struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
ModerateIntensity *int `json:"moderate_intensity"`
|
||||||
|
VigorousIntensity *int `json:"vigorous_intensity"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDailyIntensityMinutes() *DailyIntensityMinutes {
|
||||||
|
return &DailyIntensityMinutes{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_INTENSITY_PATH + "/daily/{start}/{end}",
|
||||||
|
PageSize: 28,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
27
garth/stats/sleep.go
Normal file
27
garth/stats/sleep.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const BASE_SLEEP_PATH = "/usersummary-service/stats/sleep"
|
||||||
|
|
||||||
|
type DailySleep struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
TotalSleepTime *int `json:"total_sleep_time"`
|
||||||
|
RemSleepTime *int `json:"rem_sleep_time"`
|
||||||
|
DeepSleepTime *int `json:"deep_sleep_time"`
|
||||||
|
LightSleepTime *int `json:"light_sleep_time"`
|
||||||
|
AwakeTime *int `json:"awake_time"`
|
||||||
|
SleepScore *int `json:"sleep_score"`
|
||||||
|
SleepStartTimestamp *int64 `json:"sleep_start_timestamp"`
|
||||||
|
SleepEndTimestamp *int64 `json:"sleep_end_timestamp"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDailySleep() *DailySleep {
|
||||||
|
return &DailySleep{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_SLEEP_PATH + "/daily/{start}/{end}",
|
||||||
|
PageSize: 28,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
41
garth/stats/steps.go
Normal file
41
garth/stats/steps.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const BASE_STEPS_PATH = "/usersummary-service/stats/steps"
|
||||||
|
|
||||||
|
type DailySteps struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
TotalSteps *int `json:"total_steps"`
|
||||||
|
TotalDistance *int `json:"total_distance"`
|
||||||
|
StepGoal int `json:"step_goal"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDailySteps() *DailySteps {
|
||||||
|
return &DailySteps{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_STEPS_PATH + "/daily/{start}/{end}",
|
||||||
|
PageSize: 28,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeeklySteps struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
TotalSteps int `json:"total_steps"`
|
||||||
|
AverageSteps float64 `json:"average_steps"`
|
||||||
|
AverageDistance float64 `json:"average_distance"`
|
||||||
|
TotalDistance float64 `json:"total_distance"`
|
||||||
|
WellnessDataDaysCount int `json:"wellness_data_days_count"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeeklySteps() *WeeklySteps {
|
||||||
|
return &WeeklySteps{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_STEPS_PATH + "/weekly/{end}/{period}",
|
||||||
|
PageSize: 52,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
24
garth/stats/stress.go
Normal file
24
garth/stats/stress.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const BASE_STRESS_PATH = "/usersummary-service/stats/stress"
|
||||||
|
|
||||||
|
type DailyStress struct {
|
||||||
|
CalendarDate time.Time `json:"calendar_date"`
|
||||||
|
OverallStressLevel int `json:"overall_stress_level"`
|
||||||
|
RestStressDuration *int `json:"rest_stress_duration"`
|
||||||
|
LowStressDuration *int `json:"low_stress_duration"`
|
||||||
|
MediumStressDuration *int `json:"medium_stress_duration"`
|
||||||
|
HighStressDuration *int `json:"high_stress_duration"`
|
||||||
|
BaseStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDailyStress() *DailyStress {
|
||||||
|
return &DailyStress{
|
||||||
|
BaseStats: BaseStats{
|
||||||
|
Path: BASE_STRESS_PATH + "/daily/{start}/{end}",
|
||||||
|
PageSize: 28,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user