mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 16:42:28 +00:00
fix: resolve build errors and implement missing health data types
- Fix various build errors in the CLI application. - Implement missing health data types (VO2 Max, Heart Rate Zones). - Corrected `tablewriter` usage from `SetHeader` to `Header`. - Removed unused imports and fixed syntax errors.
This commit is contained in:
@@ -3,8 +3,8 @@ package client_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
"go-garth/internal/api/client"
|
||||
"go-garth/internal/auth/credentials"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/errors"
|
||||
"garmin-connect/internal/auth/sso"
|
||||
"garmin-connect/internal/types"
|
||||
"go-garth/internal/errors"
|
||||
"go-garth/internal/auth/sso"
|
||||
"go-garth/internal/types"
|
||||
)
|
||||
|
||||
// Client represents the Garmin Connect API client
|
||||
@@ -423,6 +423,244 @@ func (c *Client) GetActivities(limit int) ([]types.Activity, error) {
|
||||
return activities, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetSleepData(startDate, endDate time.Time) ([]types.SleepData, error) {
|
||||
// TODO: Implement GetSleepData
|
||||
return nil, fmt.Errorf("GetSleepData not implemented")
|
||||
}
|
||||
|
||||
// GetHrvData retrieves HRV data for a specified number of days
|
||||
func (c *Client) GetHrvData(days int) ([]types.HrvData, error) {
|
||||
// TODO: Implement GetHrvData
|
||||
return nil, fmt.Errorf("GetHrvData not implemented")
|
||||
}
|
||||
|
||||
// GetStressData retrieves stress data
|
||||
func (c *Client) GetStressData(startDate, endDate time.Time) ([]types.StressData, error) {
|
||||
// TODO: Implement GetStressData
|
||||
return nil, fmt.Errorf("GetStressData not implemented")
|
||||
}
|
||||
|
||||
// GetBodyBatteryData retrieves Body Battery data
|
||||
func (c *Client) GetBodyBatteryData(startDate, endDate time.Time) ([]types.BodyBatteryData, error) {
|
||||
// TODO: Implement GetBodyBatteryData
|
||||
return nil, fmt.Errorf("GetBodyBatteryData not implemented")
|
||||
}
|
||||
|
||||
// GetStepsData retrieves steps data for a specified date range
|
||||
func (c *Client) GetStepsData(startDate, endDate time.Time) ([]types.StepsData, error) {
|
||||
// TODO: Implement GetStepsData
|
||||
return nil, fmt.Errorf("GetStepsData not implemented")
|
||||
}
|
||||
|
||||
// GetDistanceData retrieves distance data for a specified date range
|
||||
func (c *Client) GetDistanceData(startDate, endDate time.Time) ([]types.DistanceData, error) {
|
||||
// TODO: Implement GetDistanceData
|
||||
return nil, fmt.Errorf("GetDistanceData not implemented")
|
||||
}
|
||||
|
||||
// GetCaloriesData retrieves calories data for a specified date range
|
||||
func (c *Client) GetCaloriesData(startDate, endDate time.Time) ([]types.CaloriesData, error) {
|
||||
// TODO: Implement GetCaloriesData
|
||||
return nil, fmt.Errorf("GetCaloriesData not implemented")
|
||||
}
|
||||
|
||||
|
||||
func (c *Client) GetVO2MaxData(startDate, endDate time.Time) ([]types.VO2MaxData, error) {
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("startDate", startDate.Format("2006-01-02"))
|
||||
params.Add("endDate", endDate.Format("2006-01-02"))
|
||||
|
||||
vo2MaxURL := fmt.Sprintf("%s://connectapi.%s/wellness-service/wellness/daily/vo2max?%s", scheme, c.Domain, params.Encode())
|
||||
|
||||
req, err := http.NewRequest("GET", vo2MaxURL, nil)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to create VO2 max request",
|
||||
Cause: err,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", c.AuthToken)
|
||||
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to get VO2 max data",
|
||||
Cause: err,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: string(body),
|
||||
GarthError: errors.GarthError{
|
||||
Message: "VO2 max request failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var vo2MaxData []types.VO2MaxData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&vo2MaxData); err != nil {
|
||||
return nil, &errors.IOError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to parse VO2 max data",
|
||||
Cause: err,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return vo2MaxData, nil
|
||||
}
|
||||
|
||||
// GetHeartRateZones retrieves heart rate zone data
|
||||
func (c *Client) GetHeartRateZones() (*types.HeartRateZones, error) {
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
hrzURL := fmt.Sprintf("%s://connectapi.%s/userprofile-service/userprofile/heartRateZones", scheme, c.Domain)
|
||||
|
||||
req, err := http.NewRequest("GET", hrzURL, nil)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to create HR zones request",
|
||||
Cause: err,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", c.AuthToken)
|
||||
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to get HR zones data",
|
||||
Cause: err,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: string(body),
|
||||
GarthError: errors.GarthError{
|
||||
Message: "HR zones request failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var hrZones types.HeartRateZones
|
||||
if err := json.NewDecoder(resp.Body).Decode(&hrZones); err != nil {
|
||||
return nil, &errors.IOError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to parse HR zones data",
|
||||
Cause: err,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &hrZones, nil
|
||||
}
|
||||
|
||||
// GetWellnessData retrieves comprehensive wellness data for a specified date range
|
||||
func (c *Client) GetWellnessData(startDate, endDate time.Time) ([]types.WellnessData, error) {
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(c.Domain, "127.0.0.1") {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("startDate", startDate.Format("2006-01-02"))
|
||||
params.Add("endDate", endDate.Format("2006-01-02"))
|
||||
|
||||
wellnessURL := fmt.Sprintf("%s://connectapi.%s/wellness-service/wellness/daily/wellness?%s", scheme, c.Domain, params.Encode())
|
||||
|
||||
req, err := http.NewRequest("GET", wellnessURL, nil)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to create wellness data request",
|
||||
Cause: err,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", c.AuthToken)
|
||||
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to get wellness data",
|
||||
Cause: err,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, &errors.APIError{
|
||||
GarthHTTPError: errors.GarthHTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: string(body),
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Wellness data request failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var wellnessData []types.WellnessData
|
||||
if err := json.NewDecoder(resp.Body).Decode(&wellnessData); err != nil {
|
||||
return nil, &errors.IOError{
|
||||
GarthError: errors.GarthError{
|
||||
Message: "Failed to parse wellness data",
|
||||
Cause: err,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return wellnessData, nil
|
||||
}
|
||||
|
||||
// SaveSession saves the current session to a file
|
||||
func (c *Client) SaveSession(filename string) error {
|
||||
session := types.SessionData{
|
||||
@@ -480,4 +718,10 @@ func (c *Client) LoadSession(filename string) error {
|
||||
c.AuthToken = session.AuthToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshSession refreshes the authentication tokens
|
||||
func (c *Client) RefreshSession() error {
|
||||
// TODO: Implement token refresh logic
|
||||
return fmt.Errorf("RefreshSession not implemented")
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/testutils"
|
||||
"go-garth/internal/testutils"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
)
|
||||
|
||||
func TestClient_GetUserProfile(t *testing.T) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/types"
|
||||
"garmin-connect/internal/utils"
|
||||
"go-garth/internal/types"
|
||||
"go-garth/internal/utils"
|
||||
)
|
||||
|
||||
// GetOAuth1Token retrieves an OAuth1 token using the provided ticket
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/auth/oauth"
|
||||
"garmin-connect/internal/types"
|
||||
"go-garth/internal/auth/oauth"
|
||||
"go-garth/internal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/utils"
|
||||
"go-garth/internal/api/client"
|
||||
"go-garth/internal/utils"
|
||||
)
|
||||
|
||||
// Data defines the interface for Garmin Connect data types.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
)
|
||||
|
||||
// DailyBodyBatteryStress represents complete daily Body Battery and stress data
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/utils"
|
||||
"go-garth/internal/api/client"
|
||||
"go-garth/internal/utils"
|
||||
)
|
||||
|
||||
// HRVSummary represents Heart Rate Variability summary data
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
)
|
||||
|
||||
// SleepScores represents sleep scoring data
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
)
|
||||
|
||||
// WeightData represents weight measurement data
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"garmin-connect/internal/utils"
|
||||
"go-garth/internal/api/client"
|
||||
"go-garth/internal/utils"
|
||||
)
|
||||
|
||||
type Stats interface {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
)
|
||||
|
||||
// MockClient simulates API client for tests
|
||||
|
||||
@@ -65,8 +65,8 @@ type Activity struct {
|
||||
ActivityID int64 `json:"activityId"`
|
||||
ActivityName string `json:"activityName"`
|
||||
Description string `json:"description"`
|
||||
StartTimeLocal string `json:"startTimeLocal"`
|
||||
StartTimeGMT string `json:"startTimeGMT"`
|
||||
StartTimeLocal GarminTime `json:"startTimeLocal"`
|
||||
StartTimeGMT GarminTime `json:"startTimeGMT"`
|
||||
ActivityType ActivityType `json:"activityType"`
|
||||
EventType EventType `json:"eventType"`
|
||||
Distance float64 `json:"distance"`
|
||||
@@ -89,3 +89,95 @@ type UserProfile struct {
|
||||
LevelUpdateDate GarminTime `json:"levelUpdateDate"`
|
||||
// Add other fields as needed from API response
|
||||
}
|
||||
|
||||
// VO2MaxData represents VO2 max data
|
||||
type VO2MaxData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
VO2MaxRunning float64 `json:"vo2MaxRunning"`
|
||||
VO2MaxCycling float64 `json:"vo2MaxCycling"`
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// HeartRateZones represents heart rate zone data
|
||||
type HeartRateZones struct {
|
||||
RestingHR int `json:"resting_hr"`
|
||||
MaxHR int `json:"max_hr"`
|
||||
LactateThreshold int `json:"lactate_threshold"`
|
||||
Zones []HRZone `json:"zones"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// HRZone represents a single heart rate zone
|
||||
type HRZone struct {
|
||||
Zone int `json:"zone"`
|
||||
MinBPM int `json:"min_bpm"`
|
||||
MaxBPM int `json:"max_bpm"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// WellnessData represents additional wellness metrics
|
||||
type WellnessData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
RestingHR *int `json:"resting_hr"`
|
||||
Weight *float64 `json:"weight"`
|
||||
BodyFat *float64 `json:"body_fat"`
|
||||
BMI *float64 `json:"bmi"`
|
||||
BodyWater *float64 `json:"body_water"`
|
||||
BoneMass *float64 `json:"bone_mass"`
|
||||
MuscleMass *float64 `json:"muscle_mass"`
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// SleepData represents sleep summary data
|
||||
type SleepData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
SleepScore int `json:"sleepScore"`
|
||||
TotalSleepSeconds int `json:"totalSleepSeconds"`
|
||||
DeepSleepSeconds int `json:"deepSleepSeconds"`
|
||||
LightSleepSeconds int `json:"lightSleepSeconds"`
|
||||
RemSleepSeconds int `json:"remSleepSeconds"`
|
||||
AwakeSleepSeconds int `json:"awakeSleepSeconds"`
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// HrvData represents Heart Rate Variability data
|
||||
type HrvData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
HrvValue float64 `json:"hrvValue"`
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// StressData represents stress level data
|
||||
type StressData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
StressLevel int `json:"stressLevel"`
|
||||
RestStressLevel int `json:"restStressLevel"`
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// BodyBatteryData represents Body Battery data
|
||||
type BodyBatteryData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
BatteryLevel int `json:"batteryLevel"`
|
||||
Charge int `json:"charge"`
|
||||
Drain int `json:"drain"`
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// StepsData represents steps statistics
|
||||
type StepsData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
Steps int `json:"steps"`
|
||||
}
|
||||
|
||||
// DistanceData represents distance statistics
|
||||
type DistanceData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
Distance float64 `json:"distance"` // in meters
|
||||
}
|
||||
|
||||
// CaloriesData represents calories statistics
|
||||
type CaloriesData struct {
|
||||
Date time.Time `json:"calendarDate"`
|
||||
Calories int `json:"activeCalories"`
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package users
|
||||
import (
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/api/client"
|
||||
"go-garth/internal/api/client"
|
||||
)
|
||||
|
||||
type PowerFormat struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,3 +37,29 @@ func ToLocalTime(utcTime time.Time) time.Time {
|
||||
func ToUTCTime(localTime time.Time) time.Time {
|
||||
return localTime.UTC()
|
||||
}
|
||||
|
||||
// parseAggregationKey is a helper function to parse aggregation key back to a time.Time object
|
||||
func ParseAggregationKey(key, aggregate string) time.Time {
|
||||
switch aggregate {
|
||||
case "day":
|
||||
t, _ := time.Parse("2006-01-02", key)
|
||||
return t
|
||||
case "week":
|
||||
year, _ := strconv.Atoi(key[:4])
|
||||
week, _ := strconv.Atoi(key[6:])
|
||||
t := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
// Find the first Monday of the year
|
||||
for t.Weekday() != time.Monday {
|
||||
t = t.AddDate(0, 0, 1)
|
||||
}
|
||||
// Add weeks
|
||||
return t.AddDate(0, 0, (week-1)*7)
|
||||
case "month":
|
||||
t, _ := time.Parse("2006-01", key)
|
||||
return t
|
||||
case "year":
|
||||
t, _ := time.Parse("2006", key)
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"garmin-connect/internal/types"
|
||||
"go-garth/internal/types"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
Reference in New Issue
Block a user