mirror of
https://github.com/sstent/go-garth.git
synced 2025-12-06 08:01:42 +00:00
feat: Implement Phase 1B: Enhanced CLI Commands
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -10,7 +11,9 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"garmin-connect/pkg/garmin"
|
"garmin-connect/pkg/garmin"
|
||||||
)
|
)
|
||||||
@@ -131,11 +134,46 @@ func runListActivities(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Activities:")
|
outputFormat := viper.GetString("output")
|
||||||
for _, activity := range activities {
|
|
||||||
fmt.Printf("- ID: %d, Name: %s, Type: %s, Date: %s, Distance: %.2f km, Duration: %.0f s\n",
|
switch outputFormat {
|
||||||
activity.ActivityID, activity.ActivityName, activity.ActivityType,
|
case "json":
|
||||||
activity.Starttime.Format("2006-01-02 15:04:05"), activity.Distance/1000, activity.Duration)
|
data, err := json.MarshalIndent(activities, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal activities to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"ActivityID", "ActivityName", "ActivityType", "StartTime", "Distance(km)", "Duration(s)"})
|
||||||
|
for _, activity := range activities {
|
||||||
|
writer.Write([]string{
|
||||||
|
fmt.Sprintf("%d", activity.ActivityID),
|
||||||
|
activity.ActivityName,
|
||||||
|
activity.ActivityType,
|
||||||
|
activity.Starttime.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)"})
|
||||||
|
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"),
|
||||||
|
fmt.Sprintf("%.2f", activity.Distance/1000),
|
||||||
|
fmt.Sprintf("%.0f", activity.Duration),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"garmin-connect/pkg/garmin"
|
"garmin-connect/pkg/garmin"
|
||||||
)
|
)
|
||||||
@@ -50,6 +55,7 @@ var (
|
|||||||
healthDays int
|
healthDays int
|
||||||
healthWeek bool
|
healthWeek bool
|
||||||
healthYesterday bool
|
healthYesterday bool
|
||||||
|
healthAggregate string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -58,15 +64,19 @@ func init() {
|
|||||||
healthCmd.AddCommand(sleepCmd)
|
healthCmd.AddCommand(sleepCmd)
|
||||||
sleepCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
|
sleepCmd.Flags().StringVar(&healthDateFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
|
||||||
sleepCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
|
sleepCmd.Flags().StringVar(&healthDateTo, "to", "", "End date for data fetching (YYYY-MM-DD)")
|
||||||
|
sleepCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||||
|
|
||||||
healthCmd.AddCommand(hrvCmd)
|
healthCmd.AddCommand(hrvCmd)
|
||||||
hrvCmd.Flags().IntVar(&healthDays, "days", 0, "Number of past days to fetch data for")
|
hrvCmd.Flags().IntVar(&healthDays, "days", 0, "Number of past days to fetch data for")
|
||||||
|
hrvCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||||
|
|
||||||
healthCmd.AddCommand(stressCmd)
|
healthCmd.AddCommand(stressCmd)
|
||||||
stressCmd.Flags().BoolVar(&healthWeek, "week", false, "Fetch data for the current week")
|
stressCmd.Flags().BoolVar(&healthWeek, "week", false, "Fetch data for the current week")
|
||||||
|
stressCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||||
|
|
||||||
healthCmd.AddCommand(bodyBatteryCmd)
|
healthCmd.AddCommand(bodyBatteryCmd)
|
||||||
bodyBatteryCmd.Flags().BoolVar(&healthYesterday, "yesterday", false, "Fetch data for yesterday")
|
bodyBatteryCmd.Flags().BoolVar(&healthYesterday, "yesterday", false, "Fetch data for yesterday")
|
||||||
|
bodyBatteryCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSleep(cmd *cobra.Command, args []string) error {
|
func runSleep(cmd *cobra.Command, args []string) error {
|
||||||
@@ -110,15 +120,121 @@ func runSleep(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Sleep Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range sleepData {
|
if healthAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, Score: %d, Total Sleep: %s\n",
|
aggregatedSleep := make(map[string]struct {
|
||||||
data.Date.Format("2006-01-02"), data.SleepScore, (time.Duration(data.TotalSleepSeconds) * time.Second).String())
|
TotalSleepSeconds int
|
||||||
|
SleepScore int
|
||||||
|
Count int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range sleepData {
|
||||||
|
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 := aggregatedSleep[key]
|
||||||
|
entry.TotalSleepSeconds += data.TotalSleepSeconds
|
||||||
|
entry.SleepScore += data.SleepScore
|
||||||
|
entry.Count++
|
||||||
|
aggregatedSleep[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregated data back to a slice for output
|
||||||
|
sleepData = []garmin.SleepData{}
|
||||||
|
for key, entry := range aggregatedSleep {
|
||||||
|
sleepData = append(sleepData, garmin.SleepData{
|
||||||
|
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||||
|
TotalSleepSeconds: entry.TotalSleepSeconds / entry.Count,
|
||||||
|
SleepScore: entry.SleepScore / entry.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFormat := viper.GetString("output")
|
||||||
|
|
||||||
|
switch outputFormat {
|
||||||
|
case "json":
|
||||||
|
data, err := json.MarshalIndent(sleepData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal sleep data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"Date", "SleepScore", "TotalSleepSeconds", "DeepSleepSeconds", "LightSleepSeconds", "RemSleepSeconds", "AwakeSleepSeconds"})
|
||||||
|
for _, data := range sleepData {
|
||||||
|
writer.Write([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.SleepScore),
|
||||||
|
fmt.Sprintf("%d", data.TotalSleepSeconds),
|
||||||
|
fmt.Sprintf("%d", data.DeepSleepSeconds),
|
||||||
|
fmt.Sprintf("%d", data.LightSleepSeconds),
|
||||||
|
fmt.Sprintf("%d", data.RemSleepSeconds),
|
||||||
|
fmt.Sprintf("%d", data.AwakeSleepSeconds),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"Date", "Score", "Total Sleep", "Deep", "Light", "REM", "Awake"})
|
||||||
|
for _, data := range sleepData {
|
||||||
|
table.Append([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.SleepScore),
|
||||||
|
(time.Duration(data.TotalSleepSeconds) * time.Second).String(),
|
||||||
|
(time.Duration(data.DeepSleepSeconds) * time.Second).String(),
|
||||||
|
(time.Duration(data.LightSleepSeconds) * time.Second).String(),
|
||||||
|
(time.Duration(data.RemSleepSeconds) * time.Second).String(),
|
||||||
|
(time.Duration(data.AwakeSleepSeconds) * time.Second).String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
func runHrv(cmd *cobra.Command, args []string) error {
|
||||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -145,9 +261,77 @@ func runHrv(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("HRV Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range hrvData {
|
if healthAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, HRV: %.2f\n", data.Date.Format("2006-01-02"), data.HrvValue)
|
aggregatedHrv := make(map[string]struct {
|
||||||
|
HrvValue float64
|
||||||
|
Count int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range hrvData {
|
||||||
|
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 := aggregatedHrv[key]
|
||||||
|
entry.HrvValue += data.HrvValue
|
||||||
|
entry.Count++
|
||||||
|
aggregatedHrv[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregated data back to a slice for output
|
||||||
|
hrvData = []garmin.HrvData{}
|
||||||
|
for key, entry := range aggregatedHrv {
|
||||||
|
hrvData = append(hrvData, garmin.HrvData{
|
||||||
|
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||||
|
HrvValue: entry.HrvValue / float64(entry.Count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFormat := viper.GetString("output")
|
||||||
|
|
||||||
|
switch outputFormat {
|
||||||
|
case "json":
|
||||||
|
data, err := json.MarshalIndent(hrvData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal HRV data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"Date", "HRV Value"})
|
||||||
|
for _, data := range hrvData {
|
||||||
|
writer.Write([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%.2f", data.HrvValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"Date", "HRV Value"})
|
||||||
|
for _, data := range hrvData {
|
||||||
|
table.Append([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%.2f", data.HrvValue),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -187,10 +371,82 @@ func runStress(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Stress Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range stressData {
|
if healthAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, Stress Level: %d, Rest Stress Level: %d\n",
|
aggregatedStress := make(map[string]struct {
|
||||||
data.Date.Format("2006-01-02"), data.StressLevel, data.RestStressLevel)
|
StressLevel int
|
||||||
|
RestStressLevel int
|
||||||
|
Count int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range stressData {
|
||||||
|
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 := aggregatedStress[key]
|
||||||
|
entry.StressLevel += data.StressLevel
|
||||||
|
entry.RestStressLevel += data.RestStressLevel
|
||||||
|
entry.Count++
|
||||||
|
aggregatedStress[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregated data back to a slice for output
|
||||||
|
stressData = []garmin.StressData{}
|
||||||
|
for key, entry := range aggregatedStress {
|
||||||
|
stressData = append(stressData, garmin.StressData{
|
||||||
|
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||||
|
StressLevel: entry.StressLevel / entry.Count,
|
||||||
|
RestStressLevel: entry.RestStressLevel / entry.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFormat := viper.GetString("output")
|
||||||
|
|
||||||
|
switch outputFormat {
|
||||||
|
case "json":
|
||||||
|
data, err := json.MarshalIndent(stressData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal stress data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"Date", "StressLevel", "RestStressLevel"})
|
||||||
|
for _, data := range stressData {
|
||||||
|
writer.Write([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.StressLevel),
|
||||||
|
fmt.Sprintf("%d", data.RestStressLevel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"Date", "Stress Level", "Rest Stress Level"})
|
||||||
|
for _, data := range stressData {
|
||||||
|
table.Append([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.StressLevel),
|
||||||
|
fmt.Sprintf("%d", data.RestStressLevel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -227,10 +483,87 @@ func runBodyBattery(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Body Battery Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range bodyBatteryData {
|
if healthAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, Level: %d, Charge: %d, Drain: %d\n",
|
aggregatedBodyBattery := make(map[string]struct {
|
||||||
data.Date.Format("2006-01-02"), data.BatteryLevel, data.Charge, data.Drain)
|
BatteryLevel int
|
||||||
|
Charge int
|
||||||
|
Drain int
|
||||||
|
Count int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range bodyBatteryData {
|
||||||
|
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 := aggregatedBodyBattery[key]
|
||||||
|
entry.BatteryLevel += data.BatteryLevel
|
||||||
|
entry.Charge += data.Charge
|
||||||
|
entry.Drain += data.Drain
|
||||||
|
entry.Count++
|
||||||
|
aggregatedBodyBattery[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregated data back to a slice for output
|
||||||
|
bodyBatteryData = []garmin.BodyBatteryData{}
|
||||||
|
for key, entry := range aggregatedBodyBattery {
|
||||||
|
bodyBatteryData = append(bodyBatteryData, garmin.BodyBatteryData{
|
||||||
|
Date: parseAggregationKey(key, healthAggregate), // Helper to parse key back to date
|
||||||
|
BatteryLevel: entry.BatteryLevel / entry.Count,
|
||||||
|
Charge: entry.Charge / entry.Count,
|
||||||
|
Drain: entry.Drain / entry.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFormat := viper.GetString("output")
|
||||||
|
|
||||||
|
switch outputFormat {
|
||||||
|
case "json":
|
||||||
|
data, err := json.MarshalIndent(bodyBatteryData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal Body Battery data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"Date", "BatteryLevel", "Charge", "Drain"})
|
||||||
|
for _, data := range bodyBatteryData {
|
||||||
|
writer.Write([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.BatteryLevel),
|
||||||
|
fmt.Sprintf("%d", data.Charge),
|
||||||
|
fmt.Sprintf("%d", data.Drain),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"Date", "Level", "Charge", "Drain"})
|
||||||
|
for _, data := range bodyBatteryData {
|
||||||
|
table.Append([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.BatteryLevel),
|
||||||
|
fmt.Sprintf("%d", data.Charge),
|
||||||
|
fmt.Sprintf("%d", data.Drain),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"garmin-connect/pkg/garmin"
|
"garmin-connect/pkg/garmin"
|
||||||
)
|
)
|
||||||
@@ -41,6 +46,7 @@ var (
|
|||||||
statsMonth bool
|
statsMonth bool
|
||||||
statsYear bool
|
statsYear bool
|
||||||
statsFrom string
|
statsFrom string
|
||||||
|
statsAggregate string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -48,12 +54,15 @@ func init() {
|
|||||||
|
|
||||||
statsCmd.AddCommand(stepsCmd)
|
statsCmd.AddCommand(stepsCmd)
|
||||||
stepsCmd.Flags().BoolVar(&statsMonth, "month", false, "Fetch data for the current month")
|
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)
|
statsCmd.AddCommand(distanceCmd)
|
||||||
distanceCmd.Flags().BoolVar(&statsYear, "year", false, "Fetch data for the current year")
|
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)
|
statsCmd.AddCommand(caloriesCmd)
|
||||||
caloriesCmd.Flags().StringVar(&statsFrom, "from", "", "Start date for data fetching (YYYY-MM-DD)")
|
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 {
|
func runSteps(cmd *cobra.Command, args []string) error {
|
||||||
@@ -88,14 +97,108 @@ func runSteps(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Steps Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range stepsData {
|
if statsAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, Steps: %d\n", data.Date.Format("2006-01-02"), data.Steps)
|
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
|
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 {
|
func runDistance(cmd *cobra.Command, args []string) error {
|
||||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,9 +231,77 @@ func runDistance(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Distance Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range distanceData {
|
if statsAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, Distance: %.2f km\n", data.Date.Format("2006-01-02"), data.Distance/1000)
|
aggregatedDistance := make(map[string]struct {
|
||||||
|
Distance float64
|
||||||
|
Count int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range distanceData {
|
||||||
|
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 := aggregatedDistance[key]
|
||||||
|
entry.Distance += data.Distance
|
||||||
|
entry.Count++
|
||||||
|
aggregatedDistance[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregated data back to a slice for output
|
||||||
|
distanceData = []garmin.DistanceData{}
|
||||||
|
for key, entry := range aggregatedDistance {
|
||||||
|
distanceData = append(distanceData, garmin.DistanceData{
|
||||||
|
Date: parseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||||
|
Distance: entry.Distance / float64(entry.Count),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFormat := viper.GetString("output")
|
||||||
|
|
||||||
|
switch outputFormat {
|
||||||
|
case "json":
|
||||||
|
data, err := json.MarshalIndent(distanceData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal distance data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"Date", "Distance(km)"})
|
||||||
|
for _, data := range distanceData {
|
||||||
|
writer.Write([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%.2f", data.Distance/1000),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"Date", "Distance (km)"})
|
||||||
|
for _, data := range distanceData {
|
||||||
|
table.Append([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%.2f", data.Distance/1000),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -170,9 +341,77 @@ func runCalories(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Calories Data:")
|
// Apply aggregation if requested
|
||||||
for _, data := range caloriesData {
|
if statsAggregate != "" {
|
||||||
fmt.Printf("- Date: %s, Calories: %d\n", data.Date.Format("2006-01-02"), data.Calories)
|
aggregatedCalories := make(map[string]struct {
|
||||||
|
Calories int
|
||||||
|
Count int
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range caloriesData {
|
||||||
|
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 := aggregatedCalories[key]
|
||||||
|
entry.Calories += data.Calories
|
||||||
|
entry.Count++
|
||||||
|
aggregatedCalories[key] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert aggregated data back to a slice for output
|
||||||
|
caloriesData = []garmin.CaloriesData{}
|
||||||
|
for key, entry := range aggregatedCalories {
|
||||||
|
caloriesData = append(caloriesData, garmin.CaloriesData{
|
||||||
|
Date: parseAggregationKey(key, statsAggregate), // Helper to parse key back to date
|
||||||
|
Calories: entry.Calories / entry.Count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFormat := viper.GetString("output")
|
||||||
|
|
||||||
|
switch outputFormat {
|
||||||
|
case "json":
|
||||||
|
data, err := json.MarshalIndent(caloriesData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal calories data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
case "csv":
|
||||||
|
writer := csv.NewWriter(os.Stdout)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
writer.Write([]string{"Date", "Calories"})
|
||||||
|
for _, data := range caloriesData {
|
||||||
|
writer.Write([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.Calories),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "table":
|
||||||
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
|
table.SetHeader([]string{"Date", "Calories"})
|
||||||
|
for _, data := range caloriesData {
|
||||||
|
table.Append([]string{
|
||||||
|
data.Date.Format("2006-01-02"),
|
||||||
|
fmt.Sprintf("%d", data.Calories),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported output format: %s", outputFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -9,10 +9,18 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/fatih/color v1.15.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/olekukonko/errors v1.1.0 // indirect
|
||||||
|
github.com/olekukonko/ll v0.0.9 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v1.0.9 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
|||||||
19
go.sum
19
go.sum
@@ -1,6 +1,8 @@
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||||
|
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -17,10 +19,25 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||||
|
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||||
|
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||||
|
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||||
|
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||||
|
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -45,6 +62,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
|
|||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
|||||||
14
phase1.md
14
phase1.md
@@ -204,7 +204,7 @@ type ActivityDetail struct {
|
|||||||
- [x] Enhanced activity listing with filters
|
- [x] Enhanced activity listing with filters
|
||||||
- [x] Activity detail fetching
|
- [x] Activity detail fetching
|
||||||
- [x] Search functionality
|
- [x] Search functionality
|
||||||
- [ ] Table formatting for activity lists
|
- [x] Table formatting for activity lists
|
||||||
- [x] Activity download preparation (basic structure)
|
- [x] Activity download preparation (basic structure)
|
||||||
- [x] Date range filtering
|
- [x] Date range filtering
|
||||||
- [x] Activity type filtering
|
- [x] Activity type filtering
|
||||||
@@ -229,15 +229,15 @@ garth health bodybattery --yesterday
|
|||||||
**Tasks:**
|
**Tasks:**
|
||||||
- [x] Implement all health data commands
|
- [x] Implement all health data commands
|
||||||
- [x] Add date range parsing utilities
|
- [x] Add date range parsing utilities
|
||||||
- [ ] Create consistent output formatting
|
- [x] Create consistent output formatting
|
||||||
- [ ] Add data aggregation options
|
- [x] Add data aggregation options
|
||||||
- [ ] Implement caching for expensive operations
|
- [ ] Implement caching for expensive operations
|
||||||
- [x] Error handling for missing data
|
- [x] Error handling for missing data
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [x] All health commands working
|
- [x] All health commands working
|
||||||
- [x] Consistent date filtering across commands
|
- [x] Consistent date filtering across commands
|
||||||
- [ ] Proper data formatting and display
|
- [x] Proper data formatting and display
|
||||||
|
|
||||||
#### 1B.4: Statistics Commands
|
#### 1B.4: Statistics Commands
|
||||||
**Duration: 1 day**
|
**Duration: 1 day**
|
||||||
@@ -252,14 +252,14 @@ garth stats calories --from 2024-01-01
|
|||||||
**Tasks:**
|
**Tasks:**
|
||||||
- [x] Implement statistics commands
|
- [x] Implement statistics commands
|
||||||
- [x] Add aggregation periods (day, week, month, year)
|
- [x] Add aggregation periods (day, week, month, year)
|
||||||
- [ ] Create summary statistics
|
- [x] Create summary statistics
|
||||||
- [ ] Add trend analysis
|
- [ ] Add trend analysis
|
||||||
- [ ] Implement data export options
|
- [x] Implement data export options
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [x] All stats commands working
|
- [x] All stats commands working
|
||||||
- [x] Multiple aggregation options
|
- [x] Multiple aggregation options
|
||||||
- [ ] Export functionality
|
- [x] Export functionality
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user