mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 08:35:06 +00:00
This commit implements the package structure refactoring as outlined in phase1.md (Task 1A.1). Key changes include: - Reorganized packages into `pkg/garmin` for public API and `internal/` for internal implementations. - Updated all import paths to reflect the new structure. - Consolidated types and client logic into their respective new packages. - Updated `cmd/garth/main.go` to use the new public API. - Fixed various compilation and test issues encountered during the refactoring process. - Converted `internal/api/client/auth_test.go` to a functional test. This establishes a solid foundation for future enhancements and improves maintainability.
212 lines
5.6 KiB
Go
212 lines
5.6 KiB
Go
package data
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"garmin-connect/internal/api/client"
|
|
"garmin-connect/internal/utils"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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))
|
|
for _, values := range valuesArray {
|
|
if len(values) < 6 {
|
|
continue
|
|
}
|
|
|
|
// Extract values with type assertions
|
|
timestamp, _ := values[0].(int)
|
|
stressLevel, _ := values[1].(int)
|
|
heartRate, _ := values[2].(int)
|
|
rrInterval, _ := values[3].(int)
|
|
status, _ := values[4].(string)
|
|
signalQuality, _ := values[5].(float64)
|
|
|
|
readings = append(readings, HRVReading{
|
|
Timestamp: timestamp,
|
|
StressLevel: stressLevel,
|
|
HeartRate: heartRate,
|
|
RRInterval: rrInterval,
|
|
Status: status,
|
|
SignalQuality: signalQuality,
|
|
})
|
|
}
|
|
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) {
|
|
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
|
|
}
|