mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-26 17:11:42 +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:
@@ -8,14 +8,14 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"garmin-connect/pkg/garmin"
|
||||
"go-garth/pkg/garmin"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -43,7 +43,7 @@ var (
|
||||
downloadActivitiesCmd = &cobra.Command{
|
||||
Use: "download [activityID]",
|
||||
Short: "Download activity data",
|
||||
Args: cobra.RangeArgs(0, 1), RunE: runDownloadActivity,
|
||||
Args: cobra.RangeArgs(0, 1), RunE: runDownloadActivity,
|
||||
}
|
||||
|
||||
searchActivitiesCmd = &cobra.Command{
|
||||
@@ -54,15 +54,15 @@ var (
|
||||
}
|
||||
|
||||
// Flags for listActivitiesCmd
|
||||
activityLimit int
|
||||
activityOffset int
|
||||
activityType string
|
||||
activityDateFrom string
|
||||
activityDateTo string
|
||||
activityLimit int
|
||||
activityOffset int
|
||||
activityType string
|
||||
activityDateFrom string
|
||||
activityDateTo string
|
||||
|
||||
// Flags for downloadActivitiesCmd
|
||||
downloadFormat string
|
||||
outputDir string
|
||||
downloadFormat string
|
||||
outputDir string
|
||||
downloadOriginal bool
|
||||
downloadAll bool
|
||||
)
|
||||
@@ -105,8 +105,8 @@ func runListActivities(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
opts := garmin.ActivityOptions{
|
||||
Limit: activityLimit,
|
||||
Offset: activityOffset,
|
||||
Limit: activityLimit,
|
||||
Offset: activityOffset,
|
||||
ActivityType: activityType,
|
||||
}
|
||||
|
||||
@@ -152,21 +152,21 @@ func runListActivities(cmd *cobra.Command, args []string) error {
|
||||
writer.Write([]string{
|
||||
fmt.Sprintf("%d", activity.ActivityID),
|
||||
activity.ActivityName,
|
||||
activity.ActivityType,
|
||||
activity.Starttime.Format("2006-01-02 15:04:05"),
|
||||
activity.ActivityType.TypeKey,
|
||||
activity.StartTimeLocal.Format("2006-01-02 15:04:05"),
|
||||
fmt.Sprintf("%.2f", activity.Distance/1000),
|
||||
fmt.Sprintf("%.0f", activity.Duration),
|
||||
})
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"ID", "Name", "Type", "Date", "Distance (km)", "Duration (s)"})
|
||||
table.Header([]string{"ID", "Name", "Type", "Date", "Distance (km)", "Duration (s)"})
|
||||
for _, activity := range activities {
|
||||
table.Append([]string{
|
||||
fmt.Sprintf("%d", activity.ActivityID),
|
||||
activity.ActivityName,
|
||||
activity.ActivityType,
|
||||
activity.Starttime.Format("2006-01-02 15:04:05"),
|
||||
activity.ActivityType.TypeKey,
|
||||
activity.StartTimeLocal.Format("2006-01-02 15:04:05"),
|
||||
fmt.Sprintf("%.2f", activity.Distance/1000),
|
||||
fmt.Sprintf("%.0f", activity.Duration),
|
||||
})
|
||||
@@ -204,7 +204,7 @@ func runGetActivity(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Activity Details (ID: %d):\n", activityDetail.ActivityID)
|
||||
fmt.Printf(" Name: %s\n", activityDetail.ActivityName)
|
||||
fmt.Printf(" Type: %s\n", activityDetail.ActivityType)
|
||||
fmt.Printf(" Date: %s\n", activityDetail.Starttime.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf(" Date: %s\n", activityDetail.StartTimeLocal.Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf(" Distance: %.2f km\n", activityDetail.Distance/1000)
|
||||
fmt.Printf(" Duration: %.0f s\n", activityDetail.Duration)
|
||||
fmt.Printf(" Description: %s\n", activityDetail.Description)
|
||||
@@ -213,6 +213,10 @@ func runGetActivity(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
||||
var wg sync.WaitGroup
|
||||
const concurrencyLimit = 5 // Limit concurrent downloads
|
||||
sem := make(chan struct{}, concurrencyLimit)
|
||||
|
||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
@@ -292,7 +296,7 @@ func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
||||
defer func() { <-sem }()
|
||||
|
||||
if downloadFormat == "csv" {
|
||||
activityDetail, err := garminClient.GetActivity(activity.ActivityID)
|
||||
activityDetail, err := garminClient.GetActivity(int(activity.ActivityID))
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to get activity details for CSV export for activity %d: %v\n", activity.ActivityID, err)
|
||||
bar.Add(1)
|
||||
@@ -323,8 +327,8 @@ func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
||||
writer.Write([]string{
|
||||
fmt.Sprintf("%d", activityDetail.ActivityID),
|
||||
activityDetail.ActivityName,
|
||||
activityDetail.ActivityType,
|
||||
activityDetail.Starttime.Format("2006-01-02 15:04:05"),
|
||||
activityDetail.ActivityType.TypeKey,
|
||||
activityDetail.StartTimeLocal.Format("2006-01-02 15:04:05"),
|
||||
fmt.Sprintf("%.2f", activityDetail.Distance/1000),
|
||||
fmt.Sprintf("%.0f", activityDetail.Duration),
|
||||
activityDetail.Description,
|
||||
@@ -332,35 +336,39 @@ func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
||||
|
||||
fmt.Printf("Activity %d summary exported to %s\n", activity.ActivityID, outputPath)
|
||||
} else {
|
||||
filename := fmt.Sprintf("%d.%s", activity.ActivityID, downloadFormat)
|
||||
if downloadOriginal {
|
||||
filename = fmt.Sprintf("%d_original.fit", activity.ActivityID) // Assuming original is .fit
|
||||
}
|
||||
outputPath := filepath.Join(outputDir, filename)
|
||||
|
||||
// Check if file already exists
|
||||
if _, err := os.Stat(outputPath); err == nil {
|
||||
fmt.Printf("Skipping activity %d: file already exists at %s\n", activity.ActivityID, outputPath)
|
||||
bar.Add(1)
|
||||
return
|
||||
} else if !os.IsNotExist(err) {
|
||||
fmt.Printf("Warning: Failed to check existence of file %s for activity %d: %v\n", outputPath, activity.ActivityID, err)
|
||||
bar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
opts := garmin.DownloadOptions{
|
||||
Format: downloadFormat,
|
||||
OutputDir: outputDir,
|
||||
Original: downloadOriginal,
|
||||
Filename: filename, // Pass filename to opts
|
||||
}
|
||||
|
||||
outputPath = filepath.Join(outputDir, filename)
|
||||
}
|
||||
|
||||
// Check if file already exists
|
||||
if _, err := os.Stat(outputPath); err == nil {
|
||||
fmt.Printf("Skipping activity %d: file already exists at %s\n", activity.ActivityID, outputPath)
|
||||
bar.Add(1)
|
||||
return
|
||||
} else if !os.IsNotExist(err) {
|
||||
fmt.Printf("Warning: Failed to check existence of file %s for activity %d: %v\n", outputPath, activity.ActivityID, err)
|
||||
bar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading activity %d in %s format to %s...\n", activity.ActivityID, downloadFormat, outputDir)
|
||||
if err := garminClient.DownloadActivity(activity.ActivityID, opts); err != nil {
|
||||
fmt.Printf("Warning: Failed to download activity %d: %v\n", activity.ActivityID, err)
|
||||
bar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Activity %d downloaded successfully.\n", activity.ActivityID) }
|
||||
fmt.Printf("Downloading activity %d in %s format to %s...\n", activity.ActivityID, downloadFormat, outputPath)
|
||||
if err := garminClient.DownloadActivity(int(activity.ActivityID), opts); err != nil {
|
||||
fmt.Printf("Warning: Failed to download activity %d: %v\n", activity.ActivityID, err)
|
||||
bar.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Activity %d downloaded successfully.\n", activity.ActivityID)
|
||||
}
|
||||
bar.Add(1)
|
||||
}(activity)
|
||||
}
|
||||
@@ -401,8 +409,8 @@ func runSearchActivities(cmd *cobra.Command, args []string) error {
|
||||
fmt.Printf("Activities matching '%s':\n", query)
|
||||
for _, activity := range activities {
|
||||
fmt.Printf("- ID: %d, Name: %s, Type: %s, Date: %s\n",
|
||||
activity.ActivityID, activity.ActivityName, activity.ActivityType,
|
||||
activity.Starttime.Format("2006-01-02"))
|
||||
activity.ActivityID, activity.ActivityName, activity.ActivityType.TypeKey,
|
||||
activity.StartTimeLocal.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,13 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
"garmin-connect/pkg/garmin"
|
||||
"go-garth/pkg/garmin"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
"garmin-connect/pkg/garmin"
|
||||
"go-garth/internal/auth/credentials"
|
||||
"go-garth/pkg/garmin"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -34,7 +34,10 @@ var activitiesCmd = &cobra.Command{
|
||||
log.Fatalf("No existing session found. Please run 'garth login' first.")
|
||||
}
|
||||
|
||||
activities, err := garminClient.GetActivities(5)
|
||||
opts := garmin.ActivityOptions{
|
||||
Limit: 5,
|
||||
}
|
||||
activities, err := garminClient.ListActivities(opts)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get activities: %v", err)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
"garmin-connect/pkg/garmin"
|
||||
"go-garth/internal/auth/credentials"
|
||||
"go-garth/pkg/garmin"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -58,13 +58,13 @@ var dataCmd = &cobra.Command{
|
||||
|
||||
switch dataType {
|
||||
case "bodybattery":
|
||||
result, err = garminClient.GetBodyBattery(endDate)
|
||||
result, err = garminClient.GetBodyBatteryData(endDate, endDate)
|
||||
case "sleep":
|
||||
result, err = garminClient.GetSleep(endDate)
|
||||
result, err = garminClient.GetSleepData(endDate, endDate)
|
||||
case "hrv":
|
||||
result, err = garminClient.GetHRV(endDate)
|
||||
case "weight":
|
||||
result, err = garminClient.GetWeight(endDate)
|
||||
result, err = garminClient.GetHrvData(dataDays)
|
||||
// case "weight":
|
||||
// result, err = garminClient.GetWeight(endDate)
|
||||
default:
|
||||
log.Fatalf("Unknown data type: %s", dataType)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
"garmin-connect/pkg/garmin"
|
||||
"go-garth/internal/auth/credentials"
|
||||
"go-garth/pkg/garmin"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"garmin-connect/internal/auth/credentials"
|
||||
"garmin-connect/pkg/garmin"
|
||||
"go-garth/internal/auth/credentials"
|
||||
"go-garth/pkg/garmin"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -5,13 +5,16 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"garmin-connect/pkg/garmin"
|
||||
types "go-garth/internal/types"
|
||||
"go-garth/internal/utils"
|
||||
"go-garth/pkg/garmin"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,11 +52,23 @@ var (
|
||||
RunE: runBodyBattery,
|
||||
}
|
||||
|
||||
// Flags for health commands
|
||||
healthDateFrom string
|
||||
healthDateTo string
|
||||
healthDays int
|
||||
healthWeek bool
|
||||
vo2maxCmd = &cobra.Command{
|
||||
Use: "vo2max",
|
||||
Short: "Get VO2 Max data",
|
||||
Long: `Fetch VO2 Max data for a specified date range.`,
|
||||
RunE: runVO2Max,
|
||||
}
|
||||
|
||||
hrZonesCmd = &cobra.Command{
|
||||
Use: "hr-zones",
|
||||
Short: "Get Heart Rate Zones data",
|
||||
Long: `Fetch Heart Rate Zones data.`,
|
||||
RunE: runHRZones,
|
||||
}
|
||||
healthDateFrom string
|
||||
healthDateTo string
|
||||
healthDays int
|
||||
healthWeek bool
|
||||
healthYesterday bool
|
||||
healthAggregate string
|
||||
)
|
||||
@@ -77,6 +92,42 @@ func init() {
|
||||
healthCmd.AddCommand(bodyBatteryCmd)
|
||||
bodyBatteryCmd.Flags().BoolVar(&healthYesterday, "yesterday", false, "Fetch data for yesterday")
|
||||
bodyBatteryCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||
|
||||
// VO2 Max Command
|
||||
vo2maxCmd = &cobra.Command{
|
||||
Use: "vo2max",
|
||||
Short: "Get VO2 Max data",
|
||||
Long: `Fetch VO2 Max data for a specified date range.`,
|
||||
RunE: runVO2Max,
|
||||
}
|
||||
|
||||
healthCmd.AddCommand(vo2maxCmd)
|
||||
vo2maxCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
|
||||
vo2maxCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
|
||||
vo2maxCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||
|
||||
// Heart Rate Zones Command
|
||||
hrZonesCmd = &cobra.Command{
|
||||
Use: "hr-zones",
|
||||
Short: "Get Heart Rate Zones data",
|
||||
Long: `Fetch Heart Rate Zones data.`,
|
||||
RunE: runHRZones,
|
||||
}
|
||||
|
||||
healthCmd.AddCommand(hrZonesCmd)
|
||||
|
||||
// Wellness Command
|
||||
wellnessCmd = &cobra.Command{
|
||||
Use: "wellness",
|
||||
Short: "Get comprehensive wellness data",
|
||||
Long: `Fetch comprehensive wellness data including body composition and resting heart rate trends.`,
|
||||
RunE: runWellness,
|
||||
}
|
||||
|
||||
healthCmd.AddCommand(wellnessCmd)
|
||||
wellnessCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
|
||||
wellnessCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
|
||||
wellnessCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||
}
|
||||
|
||||
func runSleep(cmd *cobra.Command, args []string) error {
|
||||
@@ -152,12 +203,16 @@ func runSleep(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
sleepData = []garmin.SleepData{}
|
||||
sleepData = []types.SleepData{}
|
||||
for key, entry := range aggregatedSleep {
|
||||
sleepData = append(sleepData, garmin.SleepData{
|
||||
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||
sleepData = append(sleepData, types.SleepData{
|
||||
Date: utils.ParseAggregationKey(key, healthAggregate),
|
||||
TotalSleepSeconds: entry.TotalSleepSeconds / entry.Count,
|
||||
SleepScore: entry.SleepScore / entry.Count,
|
||||
DeepSleepSeconds: 0,
|
||||
LightSleepSeconds: 0,
|
||||
RemSleepSeconds: 0,
|
||||
AwakeSleepSeconds: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -189,7 +244,7 @@ func runSleep(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Score", "Total Sleep", "Deep", "Light", "REM", "Awake"})
|
||||
table.Header([]string{"Date", "Score", "Total Sleep", "Deep", "Light", "REM", "Awake"})
|
||||
for _, data := range sleepData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
@@ -209,32 +264,6 @@ func runSleep(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
func runHrv(cmd *cobra.Command, args []string) error {
|
||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||
if err != nil {
|
||||
@@ -291,10 +320,10 @@ func runHrv(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
hrvData = []garmin.HrvData{}
|
||||
hrvData = []types.HrvData{}
|
||||
for key, entry := range aggregatedHrv {
|
||||
hrvData = append(hrvData, garmin.HrvData{
|
||||
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||
hrvData = append(hrvData, types.HrvData{
|
||||
Date: utils.ParseAggregationKey(key, healthAggregate),
|
||||
HrvValue: entry.HrvValue / float64(entry.Count),
|
||||
})
|
||||
}
|
||||
@@ -322,7 +351,7 @@ func runHrv(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "HRV Value"})
|
||||
table.Header([]string{"Date", "HRV Value"})
|
||||
for _, data := range hrvData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
@@ -403,10 +432,10 @@ func runStress(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
stressData = []garmin.StressData{}
|
||||
stressData = []types.StressData{}
|
||||
for key, entry := range aggregatedStress {
|
||||
stressData = append(stressData, garmin.StressData{
|
||||
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||
stressData = append(stressData, types.StressData{
|
||||
Date: utils.ParseAggregationKey(key, healthAggregate),
|
||||
StressLevel: entry.StressLevel / entry.Count,
|
||||
RestStressLevel: entry.RestStressLevel / entry.Count,
|
||||
})
|
||||
@@ -436,7 +465,7 @@ func runStress(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Stress Level", "Rest Stress Level"})
|
||||
table.Header([]string{"Date", "Stress Level", "Rest Stress Level"})
|
||||
for _, data := range stressData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
@@ -517,10 +546,10 @@ func runBodyBattery(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
bodyBatteryData = []garmin.BodyBatteryData{}
|
||||
bodyBatteryData = []types.BodyBatteryData{}
|
||||
for key, entry := range aggregatedBodyBattery {
|
||||
bodyBatteryData = append(bodyBatteryData, garmin.BodyBatteryData{
|
||||
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||
bodyBatteryData = append(bodyBatteryData, types.BodyBatteryData{
|
||||
Date: utils.ParseAggregationKey(key, healthAggregate),
|
||||
BatteryLevel: entry.BatteryLevel / entry.Count,
|
||||
Charge: entry.Charge / entry.Count,
|
||||
Drain: entry.Drain / entry.Count,
|
||||
@@ -552,7 +581,7 @@ func runBodyBattery(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Level", "Charge", "Drain"})
|
||||
table.Header([]string{"Date", "Battery Level", "Charge", "Drain"})
|
||||
for _, data := range bodyBatteryData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
@@ -568,3 +597,205 @@ func runBodyBattery(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runVO2Max(cmd *cobra.Command, args []string) error {
|
||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
sessionFile := "garmin_session.json" // TODO: Make session file configurable
|
||||
if err := garminClient.LoadSession(sessionFile); err != nil {
|
||||
return fmt.Errorf("not logged in: %w", err)
|
||||
}
|
||||
|
||||
var startDate, endDate time.Time
|
||||
|
||||
if healthDateFrom != "" {
|
||||
startDate, err = time.Parse("2006-01-02", healthDateFrom)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid date format for --from: %w", err)
|
||||
}
|
||||
} else {
|
||||
startDate = time.Now().AddDate(0, 0, -7) // Default to last 7 days
|
||||
}
|
||||
|
||||
if healthDateTo != "" {
|
||||
endDate = time.Now() // Default to today
|
||||
parsedDate, err := time.Parse("2006-01-02", healthDateTo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid date format for --to: %w", err)
|
||||
}
|
||||
endDate = parsedDate
|
||||
}
|
||||
|
||||
vo2maxData, err := garminClient.GetVO2MaxData(startDate, endDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get VO2 Max data: %w", err)
|
||||
}
|
||||
|
||||
if len(vo2maxData) == 0 {
|
||||
fmt.Println("No VO2 Max data found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply aggregation if requested
|
||||
if healthAggregate != "" {
|
||||
aggregatedVO2Max := make(map[string]struct {
|
||||
VO2MaxRunning float64
|
||||
VO2MaxCycling float64
|
||||
Count int
|
||||
})
|
||||
|
||||
for _, data := range vo2maxData {
|
||||
key := ""
|
||||
switch healthAggregate {
|
||||
case "day":
|
||||
key = data.Date.Format("2006-01-02")
|
||||
case "week":
|
||||
year, week := data.Date.ISOWeek()
|
||||
key = fmt.Sprintf("%d-W%02d", year, week)
|
||||
case "month":
|
||||
key = data.Date.Format("2006-01")
|
||||
case "year":
|
||||
key = data.Date.Format("2006")
|
||||
default:
|
||||
return fmt.Errorf("unsupported aggregation period: %s", healthAggregate)
|
||||
}
|
||||
|
||||
entry := aggregatedVO2Max[key]
|
||||
entry.VO2MaxRunning += data.VO2MaxRunning
|
||||
entry.VO2MaxCycling += data.VO2MaxCycling
|
||||
entry.Count++
|
||||
aggregatedVO2Max[key] = entry
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
vo2maxData = []types.VO2MaxData{}
|
||||
for key, entry := range aggregatedVO2Max {
|
||||
vo2maxData = append(vo2maxData, types.VO2MaxData{
|
||||
Date: utils.ParseAggregationKey(key, healthAggregate),
|
||||
VO2MaxRunning: entry.VO2MaxRunning / float64(entry.Count),
|
||||
VO2MaxCycling: entry.VO2MaxCycling / float64(entry.Count),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
outputFormat := viper.GetString("output")
|
||||
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(vo2maxData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal VO2 Max data to JSON: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
case "csv":
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{"Date", "VO2MaxRunning", "VO2MaxCycling"})
|
||||
for _, data := range vo2maxData {
|
||||
writer.Write([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
fmt.Sprintf("%.2f", data.VO2MaxRunning),
|
||||
fmt.Sprintf("%.2f", data.VO2MaxCycling),
|
||||
})
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.Header([]string{"Date", "VO2 Max Running", "VO2 Max Cycling"})
|
||||
for _, data := range vo2maxData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
fmt.Sprintf("%.2f", data.VO2MaxRunning),
|
||||
fmt.Sprintf("%.2f", data.VO2MaxCycling),
|
||||
})
|
||||
}
|
||||
table.Render()
|
||||
default:
|
||||
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runHRZones(cmd *cobra.Command, args []string) error {
|
||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
sessionFile := "garmin_session.json" // TODO: Make session file configurable
|
||||
if err := garminClient.LoadSession(sessionFile); err != nil {
|
||||
return fmt.Errorf("not logged in: %w", err)
|
||||
}
|
||||
|
||||
hrZonesData, err := garminClient.GetHeartRateZones()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get Heart Rate Zones data: %w", err)
|
||||
}
|
||||
|
||||
if hrZonesData == nil {
|
||||
fmt.Println("No Heart Rate Zones data found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
outputFormat := viper.GetString("output")
|
||||
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(hrZonesData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal Heart Rate Zones data to JSON: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
case "csv":
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{"Zone", "MinBPM", "MaxBPM", "Name"})
|
||||
for _, zone := range hrZonesData.Zones {
|
||||
writer.Write([]string{
|
||||
strconv.Itoa(zone.Zone),
|
||||
strconv.Itoa(zone.MinBPM),
|
||||
strconv.Itoa(zone.MaxBPM),
|
||||
zone.Name,
|
||||
})
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.Header([]string{"Resting HR", "Max HR", "Lactate Threshold", "Updated At"})
|
||||
table.Append([]string{
|
||||
strconv.Itoa(hrZonesData.RestingHR),
|
||||
strconv.Itoa(hrZonesData.MaxHR),
|
||||
strconv.Itoa(hrZonesData.LactateThreshold),
|
||||
hrZonesData.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
table.Render()
|
||||
|
||||
fmt.Println()
|
||||
|
||||
zonesTable := tablewriter.NewWriter(os.Stdout)
|
||||
zonesTable.Header([]string{"Zone", "Min BPM", "Max BPM", "Name"})
|
||||
for _, zone := range hrZonesData.Zones {
|
||||
zonesTable.Append([]string{
|
||||
strconv.Itoa(zone.Zone),
|
||||
strconv.Itoa(zone.MinBPM),
|
||||
strconv.Itoa(zone.MaxBPM),
|
||||
zone.Name,
|
||||
})
|
||||
}
|
||||
zonesTable.Render()
|
||||
default:
|
||||
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var wellnessCmd *cobra.Command
|
||||
|
||||
func runWellness(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
@@ -11,194 +11,17 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"garmin-connect/pkg/garmin"
|
||||
types "go-garth/internal/types"
|
||||
"go-garth/internal/utils"
|
||||
"go-garth/pkg/garmin"
|
||||
)
|
||||
|
||||
var (
|
||||
statsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Manage Garmin Connect statistics",
|
||||
Long: `Provides commands to fetch various statistics like steps, distance, and calories.`,
|
||||
}
|
||||
|
||||
stepsCmd = &cobra.Command{
|
||||
Use: "steps",
|
||||
Short: "Get steps statistics",
|
||||
Long: `Fetch steps statistics for a specified period.`,
|
||||
RunE: runSteps,
|
||||
}
|
||||
|
||||
distanceCmd = &cobra.Command{
|
||||
Use: "distance",
|
||||
Short: "Get distance statistics",
|
||||
Long: `Fetch distance statistics for a specified period.`,
|
||||
RunE: runDistance,
|
||||
}
|
||||
|
||||
caloriesCmd = &cobra.Command{
|
||||
Use: "calories",
|
||||
Short: "Get calories statistics",
|
||||
Long: `Fetch calories statistics for a specified period.`,
|
||||
RunE: runCalories,
|
||||
}
|
||||
|
||||
// Flags for stats commands
|
||||
statsMonth bool
|
||||
statsYear bool
|
||||
statsFrom string
|
||||
statsYear bool
|
||||
statsAggregate string
|
||||
statsFrom string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(statsCmd)
|
||||
|
||||
statsCmd.AddCommand(stepsCmd)
|
||||
stepsCmd.Flags().BoolVar(&statsMonth, "month", false, "Fetch data for the current month")
|
||||
stepsCmd.Flags().StringVar(&statsAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||
|
||||
statsCmd.AddCommand(distanceCmd)
|
||||
distanceCmd.Flags().BoolVar(&statsYear, "year", false, "Fetch data for the current year")
|
||||
distanceCmd.Flags().StringVar(&statsAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||
|
||||
statsCmd.AddCommand(caloriesCmd)
|
||||
caloriesCmd.Flags().StringVar(&statsFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
|
||||
caloriesCmd.Flags().StringVar(&statsAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||
}
|
||||
|
||||
func runSteps(cmd *cobra.Command, args []string) error {
|
||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create client: %w", err)
|
||||
}
|
||||
|
||||
sessionFile := "garmin_session.json" // TODO: Make session file configurable
|
||||
if err := garminClient.LoadSession(sessionFile); err != nil {
|
||||
return fmt.Errorf("not logged in: %w", err)
|
||||
}
|
||||
|
||||
var startDate, endDate time.Time
|
||||
if statsMonth {
|
||||
now := time.Now()
|
||||
startDate = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
endDate = startDate.AddDate(0, 1, -1) // Last day of the month
|
||||
} else {
|
||||
// Default to today if no specific range or month is given
|
||||
startDate = time.Now()
|
||||
endDate = time.Now()
|
||||
}
|
||||
|
||||
stepsData, err := garminClient.GetStepsData(startDate, endDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get steps data: %w", err)
|
||||
}
|
||||
|
||||
if len(stepsData) == 0 {
|
||||
fmt.Println("No steps data found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply aggregation if requested
|
||||
if statsAggregate != "" {
|
||||
aggregatedSteps := make(map[string]struct {
|
||||
Steps int
|
||||
Count int
|
||||
})
|
||||
|
||||
for _, data := range stepsData {
|
||||
key := ""
|
||||
switch statsAggregate {
|
||||
case "day":
|
||||
key = data.Date.Format("2006-01-02")
|
||||
case "week":
|
||||
year, week := data.Date.ISOWeek()
|
||||
key = fmt.Sprintf("%d-W%02d", year, week)
|
||||
case "month":
|
||||
key = data.Date.Format("2006-01")
|
||||
case "year":
|
||||
key = data.Date.Format("2006")
|
||||
default:
|
||||
return fmt.Errorf("unsupported aggregation period: %s", statsAggregate)
|
||||
}
|
||||
|
||||
entry := aggregatedSteps[key]
|
||||
entry.Steps += data.Steps
|
||||
entry.Count++
|
||||
aggregatedSteps[key] = entry
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
stepsData = []garmin.StepsData{}
|
||||
for key, entry := range aggregatedSteps {
|
||||
stepsData = append(stepsData, garmin.StepsData{
|
||||
Date: parseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||
Steps: entry.Steps / entry.Count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
outputFormat := viper.GetString("output")
|
||||
|
||||
switch outputFormat {
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(stepsData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal steps data to JSON: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
case "csv":
|
||||
writer := csv.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
writer.Write([]string{"Date", "Steps"})
|
||||
for _, data := range stepsData {
|
||||
writer.Write([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
fmt.Sprintf("%d", data.Steps),
|
||||
})
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Steps"})
|
||||
for _, data := range stepsData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
fmt.Sprintf("%d", data.Steps),
|
||||
})
|
||||
}
|
||||
table.Render()
|
||||
default:
|
||||
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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{}
|
||||
}
|
||||
|
||||
func runDistance(cmd *cobra.Command, args []string) error {
|
||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||
if err != nil {
|
||||
@@ -261,10 +84,10 @@ func runDistance(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
distanceData = []garmin.DistanceData{}
|
||||
distanceData = []types.DistanceData{}
|
||||
for key, entry := range aggregatedDistance {
|
||||
distanceData = append(distanceData, garmin.DistanceData{
|
||||
Date: parseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||
distanceData = append(distanceData, types.DistanceData{
|
||||
Date: utils.ParseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||
Distance: entry.Distance / float64(entry.Count),
|
||||
})
|
||||
}
|
||||
@@ -292,7 +115,7 @@ func runDistance(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Distance (km)"})
|
||||
table.Header([]string{"Date", "Distance (km)"})
|
||||
for _, data := range distanceData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
@@ -371,10 +194,10 @@ func runCalories(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// Convert aggregated data back to a slice for output
|
||||
caloriesData = []garmin.CaloriesData{}
|
||||
caloriesData = []types.CaloriesData{}
|
||||
for key, entry := range aggregatedCalories {
|
||||
caloriesData = append(caloriesData, garmin.CaloriesData{
|
||||
Date: parseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||
caloriesData = append(caloriesData, types.CaloriesData{
|
||||
Date: utils.ParseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||
Calories: entry.Calories / entry.Count,
|
||||
})
|
||||
}
|
||||
@@ -402,7 +225,7 @@ func runCalories(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
case "table":
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Date", "Calories"})
|
||||
table.Header([]string{"Date", "Calories"})
|
||||
for _, data := range caloriesData {
|
||||
table.Append([]string{
|
||||
data.Date.Format("2006-01-02"),
|
||||
|
||||
Reference in New Issue
Block a user