porting - part 5 done

This commit is contained in:
2025-09-07 13:44:50 -07:00
parent ead942b122
commit cabffe9464
8 changed files with 287 additions and 30 deletions

View File

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

View File

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

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