mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 16:42:28 +00:00
porting - part 5 done
This commit is contained in:
@@ -67,8 +67,8 @@ func (b *BaseData) Get(day time.Time, c *client.Client) (interface{}, error) {
|
||||
// Returns:
|
||||
//
|
||||
// []interface{}: Slice of results (order matches date range)
|
||||
// error: First error encountered during processing, if any
|
||||
func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers int) ([]interface{}, error) {
|
||||
// []error: Slice of errors encountered during processing
|
||||
func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers int) ([]interface{}, []error) {
|
||||
if maxWorkers < 1 {
|
||||
maxWorkers = 1
|
||||
}
|
||||
@@ -82,8 +82,7 @@ func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers in
|
||||
var wg sync.WaitGroup
|
||||
workCh := make(chan time.Time, days)
|
||||
resultsCh := make(chan interface{}, days)
|
||||
errCh := make(chan error, 1)
|
||||
done := make(chan bool)
|
||||
errCh := make(chan error, days)
|
||||
|
||||
// Worker function
|
||||
worker := func() {
|
||||
@@ -91,11 +90,8 @@ func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers in
|
||||
for date := range workCh {
|
||||
result, err := b.Get(date, c)
|
||||
if err != nil {
|
||||
select {
|
||||
case errCh <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
errCh <- err
|
||||
continue
|
||||
}
|
||||
resultsCh <- result
|
||||
}
|
||||
@@ -115,28 +111,34 @@ func (b *BaseData) List(end time.Time, days int, c *client.Client, maxWorkers in
|
||||
close(workCh)
|
||||
}()
|
||||
|
||||
// Close results channel when all workers finish
|
||||
// Close channels when all workers finish
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsCh)
|
||||
done <- true
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
// Collect results and errors
|
||||
var results []interface{}
|
||||
var err error
|
||||
var errs []error
|
||||
|
||||
collect:
|
||||
for {
|
||||
// Collect results until both channels are closed
|
||||
for resultsCh != nil || errCh != nil {
|
||||
select {
|
||||
case result := <-resultsCh:
|
||||
case result, ok := <-resultsCh:
|
||||
if !ok {
|
||||
resultsCh = nil
|
||||
continue
|
||||
}
|
||||
results = append(results, result)
|
||||
case err = <-errCh:
|
||||
break collect
|
||||
case <-done:
|
||||
break collect
|
||||
case err, ok := <-errCh:
|
||||
if !ok {
|
||||
errCh = nil
|
||||
continue
|
||||
}
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return results, err
|
||||
return results, errs
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ func TestBaseData_List(t *testing.T) {
|
||||
maxWorkers := 3
|
||||
|
||||
// Execute
|
||||
results, err := mockData.List(end, days, c, maxWorkers)
|
||||
results, errs := mockData.List(end, days, c, maxWorkers)
|
||||
|
||||
// Verify
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, errs)
|
||||
assert.Len(t, results, days)
|
||||
assert.Contains(t, results, "data for 2023-06-15")
|
||||
assert.Contains(t, results, "data for 2023-06-11")
|
||||
@@ -65,10 +65,10 @@ func TestBaseData_List_ErrorHandling(t *testing.T) {
|
||||
maxWorkers := 2
|
||||
|
||||
// Execute
|
||||
results, err := mockData.List(end, days, c, maxWorkers)
|
||||
results, errs := mockData.List(end, days, c, maxWorkers)
|
||||
|
||||
// Verify
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "bad luck day", err.Error())
|
||||
assert.Len(t, results, 4) // Should have some results before error
|
||||
assert.Len(t, errs, 1)
|
||||
assert.Equal(t, "bad luck day", errs[0].Error())
|
||||
assert.Len(t, results, 4) // Should have results for non-error days
|
||||
}
|
||||
|
||||
120
garth/data/body_battery.go
Normal file
120
garth/data/body_battery.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth/client"
|
||||
)
|
||||
|
||||
// DailyBodyBatteryStress represents complete daily Body Battery and stress data
|
||||
type DailyBodyBatteryStress 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"`
|
||||
StressChartValueOffset int `json:"stressChartValueOffset"`
|
||||
StressChartYAxisOrigin int `json:"stressChartYAxisOrigin"`
|
||||
StressValuesArray [][]int `json:"stressValuesArray"`
|
||||
BodyBatteryValuesArray [][]any `json:"bodyBatteryValuesArray"`
|
||||
}
|
||||
|
||||
// BodyBatteryEvent represents a Body Battery impact event
|
||||
type BodyBatteryEvent struct {
|
||||
EventType string `json:"eventType"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// BodyBatteryData represents legacy Body Battery events data
|
||||
type BodyBatteryData struct {
|
||||
Event *BodyBatteryEvent `json:"event"`
|
||||
ActivityName string `json:"activityName"`
|
||||
ActivityType string `json:"activityType"`
|
||||
ActivityID string `json:"activityId"`
|
||||
AverageStress float64 `json:"averageStress"`
|
||||
StressValuesArray [][]int `json:"stressValuesArray"`
|
||||
BodyBatteryValuesArray [][]any `json:"bodyBatteryValuesArray"`
|
||||
}
|
||||
|
||||
// BodyBatteryReading represents an individual Body Battery reading
|
||||
type BodyBatteryReading struct {
|
||||
Timestamp int `json:"timestamp"`
|
||||
Status string `json:"status"`
|
||||
Level int `json:"level"`
|
||||
Version float64 `json:"version"`
|
||||
}
|
||||
|
||||
// StressReading represents an individual stress reading
|
||||
type StressReading struct {
|
||||
Timestamp int `json:"timestamp"`
|
||||
StressLevel int `json:"stressLevel"`
|
||||
}
|
||||
|
||||
// ParseBodyBatteryReadings converts body battery values array to structured readings
|
||||
func ParseBodyBatteryReadings(valuesArray [][]any) []BodyBatteryReading {
|
||||
readings := make([]BodyBatteryReading, 0)
|
||||
for _, values := range valuesArray {
|
||||
if len(values) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
timestamp, ok1 := values[0].(int)
|
||||
status, ok2 := values[1].(string)
|
||||
level, ok3 := values[2].(int)
|
||||
version, ok4 := values[3].(float64)
|
||||
|
||||
if !ok1 || !ok2 || !ok3 || !ok4 {
|
||||
continue
|
||||
}
|
||||
|
||||
readings = append(readings, BodyBatteryReading{
|
||||
Timestamp: timestamp,
|
||||
Status: status,
|
||||
Level: level,
|
||||
Version: version,
|
||||
})
|
||||
}
|
||||
sort.Slice(readings, func(i, j int) bool {
|
||||
return readings[i].Timestamp < readings[j].Timestamp
|
||||
})
|
||||
return readings
|
||||
}
|
||||
|
||||
// ParseStressReadings converts stress values array to structured readings
|
||||
func ParseStressReadings(valuesArray [][]int) []StressReading {
|
||||
readings := make([]StressReading, 0)
|
||||
for _, values := range valuesArray {
|
||||
if len(values) != 2 {
|
||||
continue
|
||||
}
|
||||
readings = append(readings, StressReading{
|
||||
Timestamp: values[0],
|
||||
StressLevel: values[1],
|
||||
})
|
||||
}
|
||||
sort.Slice(readings, func(i, j int) bool {
|
||||
return readings[i].Timestamp < readings[j].Timestamp
|
||||
})
|
||||
return readings
|
||||
}
|
||||
|
||||
// Get implements the Data interface for DailyBodyBatteryStress
|
||||
func (d *DailyBodyBatteryStress) Get(day time.Time, client *client.Client) (any, error) {
|
||||
// Implementation to be added
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// List implements the Data interface for concurrent fetching
|
||||
func (d *DailyBodyBatteryStress) List(end time.Time, days int, client *client.Client, maxWorkers int) ([]any, error) {
|
||||
// Implementation to be added
|
||||
return []any{}, nil
|
||||
}
|
||||
122
garth/data/body_battery_test.go
Normal file
122
garth/data/body_battery_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseBodyBatteryReadings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input [][]any
|
||||
expected []BodyBatteryReading
|
||||
}{
|
||||
{
|
||||
name: "valid readings",
|
||||
input: [][]any{
|
||||
{1000, "ACTIVE", 75, 1.0},
|
||||
{2000, "ACTIVE", 70, 1.0},
|
||||
{3000, "REST", 65, 1.0},
|
||||
},
|
||||
expected: []BodyBatteryReading{
|
||||
{1000, "ACTIVE", 75, 1.0},
|
||||
{2000, "ACTIVE", 70, 1.0},
|
||||
{3000, "REST", 65, 1.0},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid readings",
|
||||
input: [][]any{
|
||||
{1000, "ACTIVE", 75}, // missing version
|
||||
{2000, "ACTIVE"}, // missing level and version
|
||||
{3000}, // only timestamp
|
||||
{"invalid", "ACTIVE", 75, 1.0}, // wrong timestamp type
|
||||
},
|
||||
expected: []BodyBatteryReading{},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: [][]any{},
|
||||
expected: []BodyBatteryReading{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParseBodyBatteryReadings(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStressReadings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input [][]int
|
||||
expected []StressReading
|
||||
}{
|
||||
{
|
||||
name: "valid readings",
|
||||
input: [][]int{
|
||||
{1000, 25},
|
||||
{2000, 30},
|
||||
{3000, 20},
|
||||
},
|
||||
expected: []StressReading{
|
||||
{1000, 25},
|
||||
{2000, 30},
|
||||
{3000, 20},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid readings",
|
||||
input: [][]int{
|
||||
{1000}, // missing stress level
|
||||
{2000, 30, 1}, // extra value
|
||||
{}, // empty
|
||||
},
|
||||
expected: []StressReading{},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: [][]int{},
|
||||
expected: []StressReading{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ParseStressReadings(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyBodyBatteryStress(t *testing.T) {
|
||||
now := time.Now()
|
||||
d := DailyBodyBatteryStress{
|
||||
CalendarDate: now,
|
||||
BodyBatteryValuesArray: [][]any{
|
||||
{1000, "ACTIVE", 75, 1.0},
|
||||
{2000, "ACTIVE", 70, 1.0},
|
||||
},
|
||||
StressValuesArray: [][]int{
|
||||
{1000, 25},
|
||||
{2000, 30},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("body battery readings", func(t *testing.T) {
|
||||
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
|
||||
assert.Len(t, readings, 2)
|
||||
assert.Equal(t, 75, readings[0].Level)
|
||||
})
|
||||
|
||||
t.Run("stress readings", func(t *testing.T) {
|
||||
readings := ParseStressReadings(d.StressValuesArray)
|
||||
assert.Len(t, readings, 2)
|
||||
assert.Equal(t, 25, readings[0].StressLevel)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user