sync - build broken

This commit is contained in:
2025-09-20 15:21:49 -07:00
parent c1993ba022
commit 626c473b01
94 changed files with 8471 additions and 1053 deletions

View File

@@ -1,130 +0,0 @@
package data
import (
"errors"
"sync"
"time"
"go-garth/internal/api/client"
"go-garth/internal/utils"
)
// Data defines the interface for Garmin Connect data types.
// Concrete data types (BodyBattery, HRV, Sleep, etc.) must implement this interface.
//
// The Get method retrieves data for a single day.
// The List method concurrently retrieves data for a range of days.
type Data interface {
Get(day time.Time, c *client.Client) (interface{}, error)
List(end time.Time, days int, c *client.Client, maxWorkers int) ([]interface{}, error)
}
// BaseData provides a reusable implementation for data types to embed.
// It handles the concurrent List() implementation while allowing concrete types
// to focus on implementing the Get() method for their specific data structure.
//
// Usage:
//
// type BodyBatteryData struct {
// data.BaseData
// // ... additional fields
// }
//
// func NewBodyBatteryData() *BodyBatteryData {
// bb := &BodyBatteryData{}
// bb.GetFunc = bb.get // Assign the concrete Get implementation
// return bb
// }
//
// func (bb *BodyBatteryData) get(day time.Time, c *client.Client) (interface{}, error) {
// // Implementation specific to body battery data
// }
type BaseData struct {
// GetFunc must be set by concrete types to implement the Get method.
// This function pointer allows BaseData to call the concrete implementation.
GetFunc func(day time.Time, c *client.Client) (interface{}, error)
}
// Get implements the Data interface by calling the configured GetFunc.
// Returns an error if GetFunc is not set.
func (b *BaseData) Get(day time.Time, c *client.Client) (interface{}, error) {
if b.GetFunc == nil {
return nil, errors.New("GetFunc not implemented for this data type")
}
return b.GetFunc(day, c)
}
// List implements concurrent data fetching using a worker pool pattern.
// This method efficiently retrieves data for multiple days by distributing
// work across a configurable number of workers (goroutines).
//
// Parameters:
//
// end: The end date of the range (inclusive)
// days: Number of days to fetch (going backwards from end date)
// c: Client instance for API access
// maxWorkers: Maximum concurrent workers (minimum 1)
//
// Returns:
//
// []interface{}: Slice of results (order matches date range)
// []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 = 10 // Match Python's MAX_WORKERS
}
dates := utils.DateRange(end, days)
// Define result type for channel
type result struct {
data interface{}
err error
}
var wg sync.WaitGroup
workCh := make(chan time.Time, days)
resultsCh := make(chan result, days)
// Worker function
worker := func() {
defer wg.Done()
for date := range workCh {
data, err := b.Get(date, c)
resultsCh <- result{data: data, err: err}
}
}
// Start workers
wg.Add(maxWorkers)
for i := 0; i < maxWorkers; i++ {
go worker()
}
// Send work
go func() {
for _, date := range dates {
workCh <- date
}
close(workCh)
}()
// Close results channel when workers are done
go func() {
wg.Wait()
close(resultsCh)
}()
var results []interface{}
var errs []error
for r := range resultsCh {
if r.err != nil {
errs = append(errs, r.err)
} else if r.data != nil {
results = append(results, r.data)
}
}
return results, errs
}

View File

@@ -6,82 +6,31 @@ import (
"sort"
"time"
"go-garth/internal/api/client"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
// 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)
func ParseBodyBatteryReadings(valuesArray [][]any) []types.BodyBatteryReading {
readings := make([]types.BodyBatteryReading, 0)
for _, values := range valuesArray {
if len(values) < 4 {
continue
}
timestamp, ok1 := values[0].(int)
timestamp, ok1 := values[0].(float64)
status, ok2 := values[1].(string)
level, ok3 := values[2].(int)
level, ok3 := values[2].(float64)
version, ok4 := values[3].(float64)
if !ok1 || !ok2 || !ok3 || !ok4 {
continue
}
readings = append(readings, BodyBatteryReading{
Timestamp: timestamp,
readings = append(readings, types.BodyBatteryReading{
Timestamp: int(timestamp),
Status: status,
Level: level,
Level: int(level),
Version: version,
})
}
@@ -91,48 +40,61 @@ func ParseBodyBatteryReadings(valuesArray [][]any) []BodyBatteryReading {
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) {
func (d *types.DetailedBodyBatteryData) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
data, err := client.ConnectAPI(path, "GET", nil, nil)
// Get main Body Battery data
path1 := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
data1, err := c.ConnectAPI(path1, "GET", nil, nil)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to get Body Battery stress data: %w", err)
}
if len(data) == 0 {
return nil, nil
// Get Body Battery events
path2 := fmt.Sprintf("/wellness-service/wellness/bodyBattery/%s", dateStr)
data2, err := c.ConnectAPI(path2, "GET", nil, nil)
if err != nil {
// Events might not be available, continue without them
data2 = []byte("[]")
}
var result DailyBodyBatteryStress
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
var result types.DetailedBodyBatteryData
if len(data1) > 0 {
if err := json.Unmarshal(data1, &result); err != nil {
return nil, fmt.Errorf("failed to parse Body Battery data: %w", err)
}
}
var events []types.BodyBatteryEvent
if len(data2) > 0 {
if err := json.Unmarshal(data2, &events); err == nil {
result.Events = events
}
}
return &result, 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
// GetCurrentLevel returns the most recent Body Battery level
func (d *types.DetailedBodyBatteryData) GetCurrentLevel() int {
if len(d.BodyBatteryValuesArray) == 0 {
return 0
}
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) == 0 {
return 0
}
return readings[len(readings)-1].Level
}
// GetDayChange returns the Body Battery change for the day
func (d *types.DetailedBodyBatteryData) GetDayChange() int {
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) < 2 {
return 0
}
return readings[len(readings)-1].Level - readings[0].Level
}

View File

@@ -2,153 +2,46 @@ package data
import (
"encoding/json"
"errors"
"fmt"
"sort"
"time"
"go-garth/internal/api/client"
"go-garth/internal/utils"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
// HRVSummary represents Heart Rate Variability summary data
type HRVSummary struct {
UserProfilePK int `json:"userProfilePk"`
CalendarDate time.Time `json:"calendarDate"`
StartTimestampGMT time.Time `json:"startTimestampGmt"`
EndTimestampGMT time.Time `json:"endTimestampGmt"`
WeeklyAvg float64 `json:"weeklyAvg"`
LastNightAvg float64 `json:"lastNightAvg"`
Baseline float64 `json:"baseline"`
// Update the existing get method in hrv.go
func (h *types.DailyHRVData) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailyHrvData/%s?date=%s",
c.GetUsername(), dateStr)
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get HRV data: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var response struct {
HRVSummary types.DailyHRVData `json:"hrvSummary"`
HRVReadings []types.HRVReading `json:"hrvReadings"`
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, fmt.Errorf("failed to parse HRV response: %w", err)
}
// Combine summary and readings
response.HRVSummary.HRVReadings = response.HRVReadings
return &response.HRVSummary, nil
}
// HRVReading represents an individual HRV reading
type HRVReading struct {
Timestamp int `json:"timestamp"`
StressLevel int `json:"stressLevel"`
HeartRate int `json:"heartRate"`
RRInterval int `json:"rrInterval"`
Status string `json:"status"`
SignalQuality float64 `json:"signalQuality"`
}
// TimestampAsTime converts the reading timestamp to time.Time using timeutils
func (r *HRVReading) TimestampAsTime() time.Time {
return utils.ParseTimestamp(r.Timestamp)
}
// RRSeconds converts the RR interval to seconds
func (r *HRVReading) RRSeconds() float64 {
return float64(r.RRInterval) / 1000.0
}
// HRVData represents complete HRV data
type HRVData struct {
UserProfilePK int `json:"userProfilePk"`
HRVSummary HRVSummary `json:"hrvSummary"`
HRVReadings []HRVReading `json:"hrvReadings"`
BaseData
}
// Validate ensures HRVSummary fields meet requirements
func (h *HRVSummary) Validate() error {
if h.WeeklyAvg < 0 {
return errors.New("WeeklyAvg must be non-negative")
}
if h.LastNightAvg < 0 {
return errors.New("LastNightAvg must be non-negative")
}
if h.Baseline < 0 {
return errors.New("Baseline must be non-negative")
}
if h.CalendarDate.IsZero() {
return errors.New("CalendarDate must be set")
}
if h.StartTimestampGMT.IsZero() || h.EndTimestampGMT.IsZero() {
return errors.New("Timestamps must be set")
}
if h.EndTimestampGMT.Before(h.StartTimestampGMT) {
return errors.New("EndTimestampGMT must be after StartTimestampGMT")
}
return nil
}
// Validate ensures HRVReading fields meet requirements
func (r *HRVReading) Validate() error {
if r.StressLevel < 0 || r.StressLevel > 100 {
return fmt.Errorf("StressLevel must be between 0-100, got %d", r.StressLevel)
}
if r.HeartRate <= 0 {
return fmt.Errorf("HeartRate must be positive, got %d", r.HeartRate)
}
if r.RRInterval <= 0 {
return fmt.Errorf("RRInterval must be positive, got %d", r.RRInterval)
}
if r.SignalQuality < 0 || r.SignalQuality > 1 {
return fmt.Errorf("SignalQuality must be between 0-1, got %f", r.SignalQuality)
}
return nil
}
// Validate ensures HRVData meets all requirements
func (h *HRVData) Validate() error {
if h.UserProfilePK <= 0 {
return errors.New("UserProfilePK must be positive")
}
if err := h.HRVSummary.Validate(); err != nil {
return fmt.Errorf("HRVSummary validation failed: %w", err)
}
for i, reading := range h.HRVReadings {
if err := reading.Validate(); err != nil {
return fmt.Errorf("HRVReading[%d] validation failed: %w", i, err)
}
}
return nil
}
// DailyVariability calculates the average RR interval for the day
func (h *HRVData) DailyVariability() float64 {
if len(h.HRVReadings) == 0 {
return 0
}
var total float64
for _, r := range h.HRVReadings {
total += r.RRSeconds()
}
return total / float64(len(h.HRVReadings))
}
// MinHRVReading returns the reading with the lowest RR interval
func (h *HRVData) MinHRVReading() HRVReading {
if len(h.HRVReadings) == 0 {
return HRVReading{}
}
min := h.HRVReadings[0]
for _, r := range h.HRVReadings {
if r.RRInterval < min.RRInterval {
min = r
}
}
return min
}
// MaxHRVReading returns the reading with the highest RR interval
func (h *HRVData) MaxHRVReading() HRVReading {
if len(h.HRVReadings) == 0 {
return HRVReading{}
}
max := h.HRVReadings[0]
for _, r := range h.HRVReadings {
if r.RRInterval > max.RRInterval {
max = r
}
}
return max
}
// ParseHRVReadings converts values array to structured readings
func ParseHRVReadings(valuesArray [][]any) []HRVReading {
readings := make([]HRVReading, 0, len(valuesArray))
// ParseHRVReadings converts body battery values array to structured readings
func ParseHRVReadings(valuesArray [][]any) []types.HRVReading {
readings := make([]types.HRVReading, 0, len(valuesArray))
for _, values := range valuesArray {
if len(values) < 6 {
continue
@@ -162,7 +55,7 @@ func ParseHRVReadings(valuesArray [][]any) []HRVReading {
status, _ := values[4].(string)
signalQuality, _ := values[5].(float64)
readings = append(readings, HRVReading{
readings = append(readings, types.HRVReading{
Timestamp: timestamp,
StressLevel: stressLevel,
HeartRate: heartRate,
@@ -175,37 +68,4 @@ func ParseHRVReadings(valuesArray [][]any) []HRVReading {
return readings[i].Timestamp < readings[j].Timestamp
})
return readings
}
// Get implements the Data interface for HRVData
func (h *HRVData) Get(day time.Time, client *client.Client) (any, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailyHrvData/%s?date=%s",
client.Username, dateStr)
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, err
}
if len(data) == 0 {
return nil, nil
}
var hrvData HRVData
if err := json.Unmarshal(data, &hrvData); err != nil {
return nil, err
}
if err := hrvData.Validate(); err != nil {
return nil, fmt.Errorf("HRV data validation failed: %w", err)
}
return hrvData, nil
}
// List implements the Data interface for concurrent fetching
func (h *HRVData) List(end time.Time, days int, client *client.Client, maxWorkers int) ([]any, error) {
// Implementation to be added
return []any{}, nil
}
}

View File

@@ -5,45 +5,27 @@ import (
"fmt"
"time"
"go-garth/internal/api/client"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
// SleepScores represents sleep scoring data
type SleepScores struct {
TotalSleepSeconds int `json:"totalSleepSeconds"`
SleepScores []struct {
StartTimeGMT time.Time `json:"startTimeGmt"`
EndTimeGMT time.Time `json:"endTimeGmt"`
SleepScore int `json:"sleepScore"`
} `json:"sleepScores"`
SleepMovement []SleepMovement `json:"sleepMovement"`
// Add other fields from Python implementation
}
// SleepMovement represents movement during sleep
type SleepMovement struct {
StartGMT time.Time `json:"startGmt"`
EndGMT time.Time `json:"endGmt"`
ActivityLevel int `json:"activityLevel"`
}
// DailySleepDTO represents daily sleep data
type DailySleepDTO struct {
UserProfilePK int `json:"userProfilePk"`
CalendarDate time.Time `json:"calendarDate"`
SleepStartTimestampGMT time.Time `json:"sleepStartTimestampGmt"`
SleepEndTimestampGMT time.Time `json:"sleepEndTimestampGmt"`
SleepScores SleepScores `json:"sleepScores"`
BaseData
SleepScores types.SleepScore `json:"sleepScores"` // Using types.SleepScore
shared.BaseData
}
// Get implements the Data interface for DailySleepDTO
func (d *DailySleepDTO) Get(day time.Time, client *client.Client) (any, error) {
func (d *DailySleepDTO) Get(day time.Time, c shared.APIClient) (any, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailySleepData/%s?nonSleepBufferMinutes=60&date=%s",
client.Username, dateStr)
c.GetUsername(), dateStr)
data, err := client.ConnectAPI(path, "GET", nil, nil)
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, err
}
@@ -54,7 +36,7 @@ func (d *DailySleepDTO) Get(day time.Time, client *client.Client) (any, error) {
var response struct {
DailySleepDTO *DailySleepDTO `json:"dailySleepDto"`
SleepMovement []SleepMovement `json:"sleepMovement"`
SleepMovement []types.SleepMovement `json:"sleepMovement"` // Using types.SleepMovement
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, err
@@ -68,7 +50,7 @@ func (d *DailySleepDTO) Get(day time.Time, client *client.Client) (any, error) {
}
// List implements the Data interface for concurrent fetching
func (d *DailySleepDTO) List(end time.Time, days int, client *client.Client, maxWorkers int) ([]any, error) {
func (d *DailySleepDTO) List(end time.Time, days int, c shared.APIClient, maxWorkers int) ([]any, error) {
// Implementation to be added
return []any{}, nil
}

View File

@@ -0,0 +1,68 @@
package data
import (
"encoding/json"
"fmt"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
func (d *types.DetailedSleepData) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailySleepData/%s?date=%s&nonSleepBufferMinutes=60",
c.GetUsername(), dateStr)
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get detailed sleep data: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var response struct {
DailySleepDTO *types.DetailedSleepData `json:"dailySleepDTO"`
SleepMovement []types.SleepMovement `json:"sleepMovement"`
RemSleepData bool `json:"remSleepData"`
SleepLevels []types.SleepLevel `json:"sleepLevels"`
SleepRestlessMoments []interface{} `json:"sleepRestlessMoments"`
RestlessMomentsCount int `json:"restlessMomentsCount"`
WellnessSpO2SleepSummaryDTO interface{} `json:"wellnessSpO2SleepSummaryDTO"`
WellnessEpochSPO2DataDTOList []interface{} `json:"wellnessEpochSPO2DataDTOList"`
WellnessEpochRespirationDataDTOList []interface{} `json:"wellnessEpochRespirationDataDTOList"`
SleepStress interface{} `json:"sleepStress"`
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, fmt.Errorf("failed to parse detailed sleep response: %w", err)
}
if response.DailySleepDTO == nil {
return nil, nil
}
// Populate additional data
response.DailySleepDTO.SleepMovement = response.SleepMovement
response.DailySleepDTO.SleepLevels = response.SleepLevels
return response.DailySleepDTO, nil
}
// GetSleepEfficiency calculates sleep efficiency percentage
func (d *types.DetailedSleepData) GetSleepEfficiency() float64 {
totalTime := d.SleepEndTimestampGMT.Sub(d.SleepStartTimestampGMT).Seconds()
sleepTime := float64(d.DeepSleepSeconds + d.LightSleepSeconds + d.RemSleepSeconds)
if totalTime == 0 {
return 0
}
return (sleepTime / totalTime) * 100
}
// GetTotalSleepTime returns total sleep time in hours
func (d *types.DetailedSleepData) GetTotalSleepTime() float64 {
totalSeconds := d.DeepSleepSeconds + d.LightSleepSeconds + d.RemSleepSeconds
return float64(totalSeconds) / 3600.0
}

57
internal/data/training.go Normal file
View File

@@ -0,0 +1,57 @@
package data
import (
"encoding/json"
"fmt"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
func (t *types.TrainingStatus) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/metrics-service/metrics/trainingStatus/%s", dateStr)
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get training status: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var result types.TrainingStatus
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse training status: %w", err)
}
return &result, nil
}
func (t *types.TrainingLoad) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
endDate := day.AddDate(0, 0, 6).Format("2006-01-02") // Get week of data
path := fmt.Sprintf("/metrics-service/metrics/trainingLoad/%s/%s", dateStr, endDate)
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get training load: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var results []types.TrainingLoad
if err := json.Unmarshal(data, &results); err != nil {
return nil, fmt.Errorf("failed to parse training load: %w", err)
}
if len(results) == 0 {
return nil, nil
}
return &results[0], nil
}

93
internal/data/vo2max.go Normal file
View File

@@ -0,0 +1,93 @@
package data
import (
"fmt"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
// VO2MaxData implements the Data interface for VO2 max retrieval
type VO2MaxData struct {
shared.BaseData
}
// NewVO2MaxData creates a new VO2MaxData instance
func NewVO2MaxData() *VO2MaxData {
vo2 := &VO2MaxData{}
vo2.GetFunc = vo2.get
return vo2
}
// get implements the specific VO2 max data retrieval logic
func (v *VO2MaxData) get(day time.Time, c shared.APIClient) (interface{}, error) {
// Primary approach: Get from user settings (most reliable)
settings, err := c.GetUserSettings()
if err != nil {
return nil, fmt.Errorf("failed to get user settings: %w", err)
}
// Extract VO2 max data from user settings
vo2Profile := &types.VO2MaxProfile{
UserProfilePK: settings.ID,
LastUpdated: time.Now(),
}
// Add running VO2 max if available
if settings.UserData.VO2MaxRunning != nil && *settings.UserData.VO2MaxRunning > 0 {
vo2Profile.Running = &types.VO2MaxEntry{
Value: *settings.UserData.VO2MaxRunning,
ActivityType: "running",
Date: day,
Source: "user_settings",
}
}
// Add cycling VO2 max if available
if settings.UserData.VO2MaxCycling != nil && *settings.UserData.VO2MaxCycling > 0 {
vo2Profile.Cycling = &types.VO2MaxEntry{
Value: *settings.UserData.VO2MaxCycling,
ActivityType: "cycling",
Date: day,
Source: "user_settings",
}
}
// If no VO2 max data found, still return valid empty profile
return vo2Profile, nil
}
// List implements concurrent fetching for multiple days
// Note: VO2 max typically doesn't change daily, so this returns the same values
func (v *VO2MaxData) List(end time.Time, days int, c shared.APIClient, maxWorkers int) ([]interface{}, []error) {
// For VO2 max, we want current values from user settings
vo2Data, err := v.get(end, c)
if err != nil {
return nil, []error{err}
}
// Return the same VO2 max data for all requested days
results := make([]interface{}, days)
for i := 0; i < days; i++ {
results[i] = vo2Data
}
return results, nil
}
// GetCurrentVO2Max is a convenience method to get current VO2 max values
func GetCurrentVO2Max(c shared.APIClient) (*types.VO2MaxProfile, error) {
vo2Data := NewVO2MaxData()
result, err := vo2Data.get(time.Now(), c)
if err != nil {
return nil, err
}
vo2Profile, ok := result.(*types.VO2MaxProfile)
if !ok {
return nil, fmt.Errorf("unexpected result type")
}
return vo2Profile, nil
}

View File

@@ -0,0 +1,70 @@
package data
import (
"testing"
"time"
"go-garth/internal/api/client"
"go-garth/internal/models"
"github.com/stretchr/testify/assert"
)
func TestVO2MaxData_Get(t *testing.T) {
// Setup
runningVO2 := 45.0
cyclingVO2 := 50.0
settings := &client.UserSettings{
ID: 12345,
UserData: client.UserData{
VO2MaxRunning: &runningVO2,
VO2MaxCycling: &cyclingVO2,
},
}
vo2Data := NewVO2MaxData()
// Mock the get function
vo2Data.GetFunc = func(day time.Time, c *client.Client) (interface{}, error) {
vo2Profile := &models.VO2MaxProfile{
UserProfilePK: settings.ID,
LastUpdated: time.Now(),
}
if settings.UserData.VO2MaxRunning != nil && *settings.UserData.VO2MaxRunning > 0 {
vo2Profile.Running = &models.VO2MaxEntry{
Value: *settings.UserData.VO2MaxRunning,
ActivityType: "running",
Date: day,
Source: "user_settings",
}
}
if settings.UserData.VO2MaxCycling != nil && *settings.UserData.VO2MaxCycling > 0 {
vo2Profile.Cycling = &models.VO2MaxEntry{
Value: *settings.UserData.VO2MaxCycling,
ActivityType: "cycling",
Date: day,
Source: "user_settings",
}
}
return vo2Profile, nil
}
// Test
result, err := vo2Data.Get(time.Now(), nil) // client is not used in this mocked get
// Assertions
assert.NoError(t, err)
assert.NotNil(t, result)
profile, ok := result.(*models.VO2MaxProfile)
assert.True(t, ok)
assert.Equal(t, 12345, profile.UserProfilePK)
assert.NotNil(t, profile.Running)
assert.Equal(t, 45.0, profile.Running.Value)
assert.Equal(t, "running", profile.Running.ActivityType)
assert.NotNil(t, profile.Cycling)
assert.Equal(t, 50.0, profile.Cycling.Value)
assert.Equal(t, "cycling", profile.Cycling.ActivityType)
}

View File

@@ -5,25 +5,12 @@ import (
"fmt"
"time"
"go-garth/internal/api/client"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
// WeightData represents weight measurement data
type WeightData struct {
UserProfilePK int `json:"userProfilePk"`
CalendarDate time.Time `json:"calendarDate"`
Timestamp time.Time `json:"timestamp"`
Weight float64 `json:"weight"` // in kilograms
BMI float64 `json:"bmi"`
BodyFatPercentage float64 `json:"bodyFatPercentage"`
BoneMass float64 `json:"boneMass"` // in kg
MuscleMass float64 `json:"muscleMass"` // in kg
Hydration float64 `json:"hydration"` // in kg
BaseData
}
// Validate checks if weight data contains valid values
func (w *WeightData) Validate() error {
func (w *types.WeightData) Validate() error {
if w.Weight <= 0 {
return fmt.Errorf("invalid weight value")
}
@@ -34,13 +21,13 @@ func (w *WeightData) Validate() error {
}
// Get implements the Data interface for WeightData
func (w *WeightData) Get(day time.Time, client *client.Client) (any, error) {
func (w *types.WeightData) Get(day time.Time, c shared.APIClient) (any, error) {
startDate := day.Format("2006-01-02")
endDate := day.Format("2006-01-02")
path := fmt.Sprintf("/weight-service/weight/dateRange?startDate=%s&endDate=%s",
startDate, endDate)
data, err := client.ConnectAPI(path, "GET", nil, nil)
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, err
}
@@ -50,7 +37,7 @@ func (w *WeightData) Get(day time.Time, client *client.Client) (any, error) {
}
var response struct {
WeightList []WeightData `json:"weightList"`
WeightList []types.WeightData `json:"weightList"`
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, err
@@ -71,8 +58,8 @@ func (w *WeightData) Get(day time.Time, client *client.Client) (any, error) {
}
// List implements the Data interface for concurrent fetching
func (w *WeightData) List(end time.Time, days int, client *client.Client, maxWorkers int) ([]any, error) {
results, errs := w.BaseData.List(end, days, client, maxWorkers)
func (w *types.WeightData) List(end time.Time, days int, c shared.APIClient, maxWorkers int) ([]any, error) {
results, errs := w.BaseData.List(end, days, c, maxWorkers)
if len(errs) > 0 {
// Return first error for now
return results, errs[0]