mirror of
https://github.com/sstent/aicyclingcoach-go.git
synced 2026-04-05 04:23:56 +00:00
sync
This commit is contained in:
130
internal/garmin/garth/data/base.go
Normal file
130
internal/garmin/garth/data/base.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/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
|
||||
}
|
||||
74
internal/garmin/garth/data/base_test.go
Normal file
74
internal/garmin/garth/data/base_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// MockData implements Data interface for testing
|
||||
type MockData struct {
|
||||
BaseData
|
||||
}
|
||||
|
||||
// MockClient simulates API client for tests
|
||||
type MockClient struct{}
|
||||
|
||||
func (mc *MockClient) Get(endpoint string) (interface{}, error) {
|
||||
if endpoint == "error" {
|
||||
return nil, errors.New("mock API error")
|
||||
}
|
||||
return "data for " + endpoint, nil
|
||||
}
|
||||
|
||||
func TestBaseData_List(t *testing.T) {
|
||||
// Setup mock data type
|
||||
mockData := &MockData{}
|
||||
mockData.GetFunc = func(day time.Time, c *client.Client) (interface{}, error) {
|
||||
return "data for " + day.Format("2006-01-02"), nil
|
||||
}
|
||||
|
||||
// Test parameters
|
||||
end := time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
days := 5
|
||||
c := &client.Client{}
|
||||
maxWorkers := 3
|
||||
|
||||
// Execute
|
||||
results, errs := mockData.List(end, days, c, maxWorkers)
|
||||
|
||||
// Verify
|
||||
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")
|
||||
}
|
||||
|
||||
func TestBaseData_List_ErrorHandling(t *testing.T) {
|
||||
// Setup mock data type that returns error on specific date
|
||||
mockData := &MockData{}
|
||||
mockData.GetFunc = func(day time.Time, c *client.Client) (interface{}, error) {
|
||||
if day.Day() == 13 {
|
||||
return nil, errors.New("bad luck day")
|
||||
}
|
||||
return "data for " + day.Format("2006-01-02"), nil
|
||||
}
|
||||
|
||||
// Test parameters
|
||||
end := time.Date(2023, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
days := 5
|
||||
c := &client.Client{}
|
||||
maxWorkers := 2
|
||||
|
||||
// Execute
|
||||
results, errs := mockData.List(end, days, c, maxWorkers)
|
||||
|
||||
// Verify
|
||||
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
|
||||
}
|
||||
153
internal/garmin/garth/data/body_battery.go
Normal file
153
internal/garmin/garth/data/body_battery.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/errors"
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/utils"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
dateStr := day.Format("2006-01-02")
|
||||
path := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseMap, ok := response.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, &errors.IOError{GarthError: errors.GarthError{
|
||||
Message: "Invalid response format"}}
|
||||
}
|
||||
|
||||
snakeResponse := utils.CamelToSnakeDict(responseMap)
|
||||
|
||||
jsonBytes, err := json.Marshal(snakeResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result DailyBodyBatteryStress
|
||||
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
122
internal/garmin/garth/data/body_battery_test.go
Normal file
122
internal/garmin/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)
|
||||
})
|
||||
}
|
||||
68
internal/garmin/garth/data/hrv.go
Normal file
68
internal/garmin/garth/data/hrv.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
// Add other fields from Python implementation
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// HRVData represents complete HRV data
|
||||
type HRVData struct {
|
||||
UserProfilePK int `json:"userProfilePk"`
|
||||
HRVSummary HRVSummary `json:"hrvSummary"`
|
||||
HRVReadings []HRVReading `json:"hrvReadings"`
|
||||
BaseData
|
||||
}
|
||||
|
||||
// ParseHRVReadings converts values array to structured readings
|
||||
func ParseHRVReadings(valuesArray [][]any) []HRVReading {
|
||||
readings := make([]HRVReading, 0)
|
||||
for _, values := range valuesArray {
|
||||
if len(values) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract values with type assertions
|
||||
// Add parsing logic based on Python implementation
|
||||
|
||||
readings = append(readings, HRVReading{
|
||||
// Initialize fields
|
||||
})
|
||||
}
|
||||
sort.Slice(readings, func(i, j int) bool {
|
||||
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) {
|
||||
// Implementation to be added
|
||||
return nil, 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
|
||||
}
|
||||
91
internal/garmin/garth/data/sleep.go
Normal file
91
internal/garmin/garth/data/sleep.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/errors"
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/utils"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Get implements the Data interface for DailySleepDTO
|
||||
func (d *DailySleepDTO) Get(day time.Time, client *client.Client) (any, error) {
|
||||
dateStr := day.Format("2006-01-02")
|
||||
path := fmt.Sprintf("/wellness-service/wellness/dailySleepData/%s?nonSleepBufferMinutes=60&date=%s",
|
||||
client.Username, dateStr)
|
||||
|
||||
response, err := client.ConnectAPI(path, "GET", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
responseMap, ok := response.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, &errors.IOError{GarthError: errors.GarthError{
|
||||
Message: "Invalid response format"}}
|
||||
}
|
||||
|
||||
snakeResponse := utils.CamelToSnakeDict(responseMap)
|
||||
|
||||
dailySleepDto, exists := snakeResponse["daily_sleep_dto"].(map[string]interface{})
|
||||
if !exists || dailySleepDto["id"] == nil {
|
||||
return nil, nil // No sleep data
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(snakeResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result struct {
|
||||
DailySleepDTO *DailySleepDTO `json:"daily_sleep_dto"`
|
||||
SleepMovement []SleepMovement `json:"sleep_movement"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// List implements the Data interface for concurrent fetching
|
||||
func (d *DailySleepDTO) List(end time.Time, days int, client *client.Client, maxWorkers int) ([]any, error) {
|
||||
// Implementation to be added
|
||||
return []any{}, nil
|
||||
}
|
||||
39
internal/garmin/garth/data/weight.go
Normal file
39
internal/garmin/garth/data/weight.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
BaseData
|
||||
}
|
||||
|
||||
// Validate checks if weight data contains valid values
|
||||
func (w *WeightData) Validate() error {
|
||||
if w.Weight <= 0 {
|
||||
return errors.New("invalid weight value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implements the Data interface for WeightData
|
||||
func (w *WeightData) 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 (w *WeightData) List(end time.Time, days int, client *client.Client, maxWorkers int) ([]any, error) {
|
||||
// Implementation to be added
|
||||
return []any{}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user