diff --git a/cmd/garth/activities.go b/cmd/garth/activities.go deleted file mode 100644 index 4eb99c2..0000000 --- a/cmd/garth/activities.go +++ /dev/null @@ -1,416 +0,0 @@ -package main - -import ( - "encoding/csv" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strconv" - "sync" - "time" - - "github.com/rodaine/table" - "github.com/schollz/progressbar/v3" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/sstent/go-garth/pkg/garmin" -) - -var ( - activitiesCmd = &cobra.Command{ - Use: "activities", - Short: "Manage Garmin Connect activities", - Long: `Provides commands to list, get details, search, and download Garmin Connect activities.`, - } - - listActivitiesCmd = &cobra.Command{ - Use: "list", - Short: "List recent activities", - Long: `List recent Garmin Connect activities with optional filters.`, - RunE: runListActivities, - } - - getActivitiesCmd = &cobra.Command{ - Use: "get [activityID]", - Short: "Get activity details", - Long: `Get detailed information for a specific Garmin Connect activity.`, - Args: cobra.ExactArgs(1), - RunE: runGetActivity, - } - - downloadActivitiesCmd = &cobra.Command{ - Use: "download [activityID]", - Short: "Download activity data", - Args: cobra.RangeArgs(0, 1), RunE: runDownloadActivity, - } - - searchActivitiesCmd = &cobra.Command{ - Use: "search", - Short: "Search activities", - Long: `Search Garmin Connect activities by a query string.`, - RunE: runSearchActivities, - } - - // Flags for listActivitiesCmd - activityLimit int - activityOffset int - activityType string - activityDateFrom string - activityDateTo string - - // Flags for downloadActivitiesCmd - downloadFormat string - outputDir string - downloadOriginal bool - downloadAll bool -) - -func init() { - rootCmd.AddCommand(activitiesCmd) - - activitiesCmd.AddCommand(listActivitiesCmd) - listActivitiesCmd.Flags().IntVar(&activityLimit, "limit", 20, "Maximum number of activities to retrieve") - listActivitiesCmd.Flags().IntVar(&activityOffset, "offset", 0, "Offset for activities list") - listActivitiesCmd.Flags().StringVar(&activityType, "type", "", "Filter activities by type (e.g., running, cycling)") - listActivitiesCmd.Flags().StringVar(&activityDateFrom, "from", "", "Start date for filtering activities (YYYY-MM-DD)") - listActivitiesCmd.Flags().StringVar(&activityDateTo, "to", "", "End date for filtering activities (YYYY-MM-DD)") - - activitiesCmd.AddCommand(getActivitiesCmd) - - activitiesCmd.AddCommand(downloadActivitiesCmd) - downloadActivitiesCmd.Flags().StringVar(&downloadFormat, "format", "gpx", "Download format (gpx, tcx, fit, csv)") - downloadActivitiesCmd.Flags().StringVar(&outputDir, "output-dir", ".", "Output directory for downloaded files") - downloadActivitiesCmd.Flags().BoolVar(&downloadOriginal, "original", false, "Download original uploaded file") - - downloadActivitiesCmd.Flags().BoolVar(&downloadAll, "all", false, "Download all activities matching filters") - downloadActivitiesCmd.Flags().StringVar(&activityType, "type", "", "Filter activities by type (e.g., running, cycling)") - downloadActivitiesCmd.Flags().StringVar(&activityDateFrom, "from", "", "Start date for filtering activities (YYYY-MM-DD)") - downloadActivitiesCmd.Flags().StringVar(&activityDateTo, "to", "", "End date for filtering activities (YYYY-MM-DD)") - - activitiesCmd.AddCommand(searchActivitiesCmd) - searchActivitiesCmd.Flags().StringP("query", "q", "", "Query string to search for activities") -} - -func runListActivities(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) - } - - opts := garmin.ActivityOptions{ - Limit: activityLimit, - Offset: activityOffset, - ActivityType: activityType, - } - - if activityDateFrom != "" { - opts.DateFrom, err = time.Parse("2006-01-02", activityDateFrom) - if err != nil { - return fmt.Errorf("invalid date format for --from: %w", err) - } - } - - if activityDateTo != "" { - opts.DateTo, err = time.Parse("2006-01-02", activityDateTo) - if err != nil { - return fmt.Errorf("invalid date format for --to: %w", err) - } - } - - activities, err := garminClient.ListActivities(opts) - if err != nil { - return fmt.Errorf("failed to list activities: %w", err) - } - - if len(activities) == 0 { - fmt.Println("No activities found.") - return nil - } - - outputFormat := viper.GetString("output") - - switch outputFormat { - case "json": - 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.TypeKey, - activity.StartTimeLocal.Format("2006-01-02 15:04:05"), - fmt.Sprintf("%.2f", activity.Distance/1000), - fmt.Sprintf("%.0f", activity.Duration), - }) - } - case "table": - tbl := table.New("ID", "Name", "Type", "Date", "Distance (km)", "Duration (s)") - for _, activity := range activities { - tbl.AddRow( - fmt.Sprintf("%d", activity.ActivityID), - activity.ActivityName, - activity.ActivityType.TypeKey, - activity.StartTimeLocal.Format("2006-01-02 15:04:05"), - fmt.Sprintf("%.2f", activity.Distance/1000), - fmt.Sprintf("%.0f", activity.Duration), - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runGetActivity(cmd *cobra.Command, args []string) error { - activityIDStr := args[0] - activityID, err := strconv.Atoi(activityIDStr) - if err != nil { - return fmt.Errorf("invalid activity ID: %w", err) - } - - 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) - } - - activityDetail, err := garminClient.GetActivity(activityID) - if err != nil { - return fmt.Errorf("failed to get activity details: %w", err) - } - - fmt.Printf("Activity Details (ID: %d):\n", activityDetail.ActivityID) - fmt.Printf(" Name: %s\n", activityDetail.ActivityName) - fmt.Printf(" Type: %s\n", activityDetail.ActivityType.TypeKey) - 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) - - return nil -} - -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) - } - - 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 activitiesToDownload []garmin.Activity - - if downloadAll || len(args) == 0 { - opts := garmin.ActivityOptions{ - ActivityType: activityType, - } - - if activityDateFrom != "" { - opts.DateFrom, err = time.Parse("2006-01-02", activityDateFrom) - if err != nil { - return fmt.Errorf("invalid date format for --from: %w", err) - } - } - - if activityDateTo != "" { - opts.DateTo, err = time.Parse("2006-01-02", activityDateTo) - if err != nil { - return fmt.Errorf("invalid date format for --to: %w", err) - } - } - - activitiesToDownload, err = garminClient.ListActivities(opts) - if err != nil { - return fmt.Errorf("failed to list activities for batch download: %w", err) - } - - if len(activitiesToDownload) == 0 { - fmt.Println("No activities found matching the filters for download.") - return nil - } - } else if len(args) == 1 { - activityIDStr := args[0] - activityID, err := strconv.Atoi(activityIDStr) - if err != nil { - return fmt.Errorf("invalid activity ID: %w", err) - } - // For single download, we need to fetch the activity details to get its name and type - activityDetail, err := garminClient.GetActivity(activityID) - if err != nil { - return fmt.Errorf("failed to get activity details for download: %w", err) - } - activitiesToDownload = []garmin.Activity{activityDetail.Activity} - } else { - return fmt.Errorf("invalid arguments: specify an activity ID or use --all with filters") - } - - fmt.Printf("Starting download of %d activities...\n", len(activitiesToDownload)) - - bar := progressbar.NewOptions(len(activitiesToDownload), - progressbar.OptionEnableColorCodes(true), - progressbar.OptionShowBytes(false), - progressbar.OptionSetWidth(15), - progressbar.OptionSetDescription("Downloading activities..."), - progressbar.OptionSetTheme(progressbar.Theme{ - Saucer: "[green]=[reset]", - SaucerPadding: " ", - BarStart: "[ ", - BarEnd: " ]", - }), - ) - - for _, activity := range activitiesToDownload { - wg.Add(1) - sem <- struct{}{} - go func(activity garmin.Activity) { - defer wg.Done() - defer func() { <-sem }() - - if downloadFormat == "csv" { - 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) - return - } - - filename := fmt.Sprintf("%d.csv", activity.ActivityID) - outputPath := filename - if outputDir != "" { - outputPath = filepath.Join(outputDir, filename) - } - - file, err := os.Create(outputPath) - if err != nil { - fmt.Printf("Warning: Failed to create CSV file for activity %d: %v\n", activity.ActivityID, err) - bar.Add(1) - return - } - defer file.Close() - - writer := csv.NewWriter(file) - defer writer.Flush() - - // Write header - writer.Write([]string{"ActivityID", "ActivityName", "ActivityType", "StartTime", "Distance(km)", "Duration(s)", "Description"}) - - // Write data - writer.Write([]string{ - fmt.Sprintf("%d", activityDetail.ActivityID), - activityDetail.ActivityName, - 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, - }) - - 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 - } - - 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) - } - - wg.Wait() - bar.Finish() - fmt.Println("All downloads finished.") - - return nil -} - -func runSearchActivities(cmd *cobra.Command, args []string) error { - query, err := cmd.Flags().GetString("query") - if err != nil || query == "" { - return fmt.Errorf("search query cannot be empty") - } - - 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) - } - - activities, err := garminClient.SearchActivities(query) - if err != nil { - return fmt.Errorf("failed to search activities: %w", err) - } - - if len(activities) == 0 { - fmt.Printf("No activities found for query '%s'.\n", query) - return nil - } - - 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.TypeKey, - activity.StartTimeLocal.Format("2006-01-02")) - } - - return nil -} diff --git a/cmd/garth/auth.go b/cmd/garth/auth.go deleted file mode 100644 index c2bed2d..0000000 --- a/cmd/garth/auth.go +++ /dev/null @@ -1,183 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "golang.org/x/term" - - "github.com/sstent/go-garth/pkg/garmin" - - "github.com/spf13/cobra" -) - -var ( - authCmd = &cobra.Command{ - Use: "auth", - Short: "Authentication management", - Long: `Manage authentication with Garmin Connect, including login, logout, and status.`, - } - - loginCmd = &cobra.Command{ - Use: "login", - Short: "Login to Garmin Connect", - Long: `Login to Garmin Connect interactively or using provided credentials.`, - RunE: runLogin, - } - - logoutCmd = &cobra.Command{ - Use: "logout", - Short: "Logout from Garmin Connect", - Long: `Clear the current Garmin Connect session.`, - RunE: runLogout, - } - - statusCmd = &cobra.Command{ - Use: "status", - Short: "Show Garmin Connect authentication status", - Long: `Display the current authentication status and session information.`, - RunE: runStatus, - } - - refreshCmd = &cobra.Command{ - Use: "refresh", - Short: "Refresh Garmin Connect session tokens", - Long: `Refresh the authentication tokens for the current Garmin Connect session.`, - RunE: runRefresh, - } - - loginEmail string - loginPassword string - passwordStdinFlag bool -) - -func init() { - rootCmd.AddCommand(authCmd) - - authCmd.AddCommand(loginCmd) - loginCmd.Flags().StringVarP(&loginEmail, "email", "e", "", "Email for Garmin Connect login") - loginCmd.Flags().BoolVarP(&passwordStdinFlag, "password-stdin", "p", false, "Read password from stdin") - - authCmd.AddCommand(logoutCmd) - authCmd.AddCommand(statusCmd) - authCmd.AddCommand(refreshCmd) -} - -func runLogin(cmd *cobra.Command, args []string) error { - var email, password string - var err error - - if loginEmail != "" { - email = loginEmail - } else { - fmt.Print("Enter Garmin Connect email: ") - _, err = fmt.Scanln(&email) - if err != nil { - return fmt.Errorf("failed to read email: %w", err) - } - } - - if passwordStdinFlag { - fmt.Print("Enter password: ") - passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return fmt.Errorf("failed to read password from stdin: %w", err) - } - password = string(passwordBytes) - fmt.Println() // Newline after password input - } else { - fmt.Print("Enter password: ") - passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return fmt.Errorf("failed to read password: %w", err) - } - password = string(passwordBytes) - fmt.Println() // Newline after password input - } - - // Create client - // TODO: Domain should be configurable - garminClient, err := garmin.NewClient("www.garmin.com") - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - // Try to load existing session first - sessionFile := "garmin_session.json" // TODO: Make session file configurable - if err := garminClient.LoadSession(sessionFile); err != nil { - fmt.Println("No existing session found or session invalid, logging in with credentials...") - - if err := garminClient.Login(email, password); err != nil { - return fmt.Errorf("login failed: %w", err) - } - - // Save session for future use - if err := garminClient.SaveSession(sessionFile); err != nil { - fmt.Printf("Failed to save session: %v\n", err) - } - } else { - fmt.Println("Loaded existing session") - } - - fmt.Println("Login successful!") - return nil -} - -func runLogout(cmd *cobra.Command, args []string) error { - sessionFile := "garmin_session.json" // TODO: Make session file configurable - - if _, err := os.Stat(sessionFile); os.IsNotExist(err) { - fmt.Println("No active session to log out from.") - return nil - } - - if err := os.Remove(sessionFile); err != nil { - return fmt.Errorf("failed to remove session file: %w", err) - } - - fmt.Println("Logged out successfully. Session cleared.") - return nil -} - -func runStatus(cmd *cobra.Command, args []string) error { - sessionFile := "garmin_session.json" // TODO: Make session file configurable - - garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(sessionFile); err != nil { - fmt.Println("Not logged in or session expired.") - return nil - } - - fmt.Println("Logged in. Session is active.") - // TODO: Add more detailed status information, e.g., session expiry - return nil -} - -func runRefresh(cmd *cobra.Command, args []string) error { - sessionFile := "garmin_session.json" // TODO: Make session file configurable - - garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(sessionFile); err != nil { - return fmt.Errorf("cannot refresh: no active session found: %w", err) - } - - fmt.Println("Attempting to refresh session...") - if err := garminClient.RefreshSession(); err != nil { - return fmt.Errorf("failed to refresh session: %w", err) - } - - if err := garminClient.SaveSession(sessionFile); err != nil { - fmt.Printf("Failed to save refreshed session: %v\n", err) - } - - fmt.Println("Session refreshed successfully.") - return nil -} diff --git a/cmd/garth/cmd/activities.go b/cmd/garth/cmd/activities.go deleted file mode 100644 index 185e19b..0000000 --- a/cmd/garth/cmd/activities.go +++ /dev/null @@ -1,67 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "time" - - "github.com/sstent/go-garth/internal/auth/credentials" - "github.com/sstent/go-garth/pkg/garmin" - - "github.com/spf13/cobra" -) - -var activitiesCmd = &cobra.Command{ - Use: "activities", - Short: "Display recent Garmin Connect activities", - Long: `Fetches and displays a list of recent activities from Garmin Connect.`, - Run: func(cmd *cobra.Command, args []string) { - // Load credentials from .env file - _, _, domain, err := credentials.LoadEnvCredentials() - if err != nil { - log.Fatalf("Failed to load credentials: %v", err) - } - - // Create client - garminClient, err := garmin.NewClient(domain) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Try to load existing session first - sessionFile := "garmin_session.json" - if err := garminClient.LoadSession(sessionFile); err != nil { - log.Fatalf("No existing session found. Please run 'garth login' first.") - } - - opts := garmin.ActivityOptions{ - Limit: 5, - } - activities, err := garminClient.ListActivities(opts) - if err != nil { - log.Fatalf("Failed to get activities: %v", err) - } - displayActivities(activities) - }, -} - -func init() { - rootCmd.AddCommand(activitiesCmd) -} - -func displayActivities(activities []garmin.Activity) { - fmt.Printf("\n=== Recent Activities ===\n") - for i, activity := range activities { - fmt.Printf("%d. %s\n", i+1, activity.ActivityName) - fmt.Printf(" Type: %s\n", activity.ActivityType.TypeKey) - fmt.Printf(" Date: %s\n", activity.StartTimeLocal) - if activity.Distance > 0 { - fmt.Printf(" Distance: %.2f km\n", activity.Distance/1000) - } - if activity.Duration > 0 { - duration := time.Duration(activity.Duration) * time.Second - fmt.Printf(" Duration: %v\n", duration.Round(time.Second)) - } - fmt.Println() - } -} diff --git a/cmd/garth/cmd/data.go b/cmd/garth/cmd/data.go deleted file mode 100644 index e6d1499..0000000 --- a/cmd/garth/cmd/data.go +++ /dev/null @@ -1,102 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "log" - "os" - "time" - - "github.com/sstent/go-garth/internal/auth/credentials" - "github.com/sstent/go-garth/pkg/garmin" - - "github.com/spf13/cobra" -) - -var ( - dataDateStr string - dataDays int - dataOutputFile string -) - -var dataCmd = &cobra.Command{ - Use: "data [type]", - Short: "Fetch various data types from Garmin Connect", - Long: `Fetch data such as bodybattery, sleep, HRV, and weight from Garmin Connect.`, - Args: cobra.ExactArgs(1), // Expects one argument: the data type - Run: func(cmd *cobra.Command, args []string) { - dataType := args[0] - - // Load credentials from .env file - _, _, domain, err := credentials.LoadEnvCredentials() - if err != nil { - log.Fatalf("Failed to load credentials: %v", err) - } - - // Create client - garminClient, err := garmin.NewClient(domain) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Try to load existing session first - sessionFile := "garmin_session.json" - if err := garminClient.LoadSession(sessionFile); err != nil { - log.Fatalf("No existing session found. Please run 'garth login' first.") - } - - endDate := time.Now().AddDate(0, 0, -1) // default to yesterday - if dataDateStr != "" { - parsedDate, err := time.Parse("2006-01-02", dataDateStr) - if err != nil { - log.Fatalf("Invalid date format: %v", err) - } - endDate = parsedDate - } - - var result interface{} - - switch dataType { - case "bodybattery": - result, err = garminClient.GetBodyBatteryData(endDate) - case "sleep": - result, err = garminClient.GetSleepData(endDate) - case "hrv": - result, err = garminClient.GetHrvData(endDate) - // case "weight": - // result, err = garminClient.GetWeight(endDate) - default: - log.Fatalf("Unknown data type: %s", dataType) - } - - if err != nil { - log.Fatalf("Failed to get %s data: %v", dataType, err) - } - - outputResult(result, dataOutputFile) - }, -} - -func init() { - rootCmd.AddCommand(dataCmd) - - dataCmd.Flags().StringVar(&dataDateStr, "date", "", "Date in YYYY-MM-DD format (default: yesterday)") - dataCmd.Flags().StringVar(&dataOutputFile, "output", "", "Output file for JSON results") - // dataCmd.Flags().IntVar(&dataDays, "days", 1, "Number of days to fetch") // Not used for single day data types -} - -func outputResult(data interface{}, outputFile string) { - jsonBytes, err := json.MarshalIndent(data, "", " ") - if err != nil { - log.Fatalf("Failed to marshal result: %v", err) - } - - if outputFile != "" { - if err := os.WriteFile(outputFile, jsonBytes, 0644); err != nil { - log.Fatalf("Failed to write output file: %v", err) - } - fmt.Printf("Results saved to %s\n", outputFile) - } else { - fmt.Println(string(jsonBytes)) - } -} diff --git a/cmd/garth/cmd/root.go b/cmd/garth/cmd/root.go deleted file mode 100644 index ecc2f15..0000000 --- a/cmd/garth/cmd/root.go +++ /dev/null @@ -1,38 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var rootCmd = &cobra.Command{ - Use: "garth", - Short: "garth is a CLI for interacting with Garmin Connect", - Long: `A command-line interface for Garmin Connect that allows you to -interact with your health and fitness data.`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} - -func init() { - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, will be global for your application. - - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.garth.yaml)") - - // Cobra also supports local flags, which will only run when this action is called directly. - // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} - diff --git a/cmd/garth/cmd/stats.go b/cmd/garth/cmd/stats.go deleted file mode 100644 index 505249c..0000000 --- a/cmd/garth/cmd/stats.go +++ /dev/null @@ -1,87 +0,0 @@ -package cmd - -import ( - "log" - "time" - - "github.com/sstent/go-garth/internal/auth/credentials" - "github.com/sstent/go-garth/pkg/garmin" - - "github.com/spf13/cobra" -) - -var ( - statsDateStr string - statsDays int - statsOutputFile string -) - -var statsCmd = &cobra.Command{ - Use: "stats [type]", - Short: "Fetch various stats types from Garmin Connect", - Long: `Fetch stats such as steps, stress, hydration, intensity, sleep, and HRV from Garmin Connect.`, - Args: cobra.ExactArgs(1), // Expects one argument: the stats type - Run: func(cmd *cobra.Command, args []string) { - statsType := args[0] - - // Load credentials from .env file - _, _, domain, err := credentials.LoadEnvCredentials() - if err != nil { - log.Fatalf("Failed to load credentials: %v", err) - } - - // Create client - garminClient, err := garmin.NewClient(domain) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Try to load existing session first - sessionFile := "garmin_session.json" - if err := garminClient.LoadSession(sessionFile); err != nil { - log.Fatalf("No existing session found. Please run 'garth login' first.") - } - - endDate := time.Now().AddDate(0, 0, -1) // default to yesterday - if statsDateStr != "" { - parsedDate, err := time.Parse("2006-01-02", statsDateStr) - if err != nil { - log.Fatalf("Invalid date format: %v", err) - } - endDate = parsedDate - } - - var stats garmin.Stats - switch statsType { - case "steps": - stats = garmin.NewDailySteps() - case "stress": - stats = garmin.NewDailyStress() - case "hydration": - stats = garmin.NewDailyHydration() - case "intensity": - stats = garmin.NewDailyIntensityMinutes() - case "sleep": - stats = garmin.NewDailySleep() - case "hrv": - stats = garmin.NewDailyHRV() - default: - log.Fatalf("Unknown stats type: %s", statsType) - } - - result, err := stats.List(endDate, statsDays, garminClient.Client) - if err != nil { - log.Fatalf("Failed to get %s stats: %v", statsType, err) - } - - outputResult(result, statsOutputFile) - }, -} - -func init() { - rootCmd.AddCommand(statsCmd) - - statsCmd.Flags().StringVar(&statsDateStr, "date", "", "Date in YYYY-MM-DD format (default: yesterday)") - statsCmd.Flags().IntVar(&statsDays, "days", 1, "Number of days to fetch") - statsCmd.Flags().StringVar(&statsOutputFile, "output", "", "Output file for JSON results") -} diff --git a/cmd/garth/cmd/tokens.go b/cmd/garth/cmd/tokens.go deleted file mode 100644 index 4e1accf..0000000 --- a/cmd/garth/cmd/tokens.go +++ /dev/null @@ -1,55 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "log" - - "github.com/sstent/go-garth/internal/auth/credentials" - "github.com/sstent/go-garth/pkg/garmin" - - "github.com/spf13/cobra" -) - -var tokensCmd = &cobra.Command{ - Use: "tokens", - Short: "Output OAuth tokens in JSON format", - Long: `Output the OAuth1 and OAuth2 tokens in JSON format after a successful login.`, - Run: func(cmd *cobra.Command, args []string) { - // Load credentials from .env file - _, _, domain, err := credentials.LoadEnvCredentials() - if err != nil { - log.Fatalf("Failed to load credentials: %v", err) - } - - // Create client - garminClient, err := garmin.NewClient(domain) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Try to load existing session first - sessionFile := "garmin_session.json" - if err := garminClient.LoadSession(sessionFile); err != nil { - log.Fatalf("No existing session found. Please run 'garth login' first.") - } - - tokens := struct { - OAuth1 *garmin.OAuth1Token `json:"oauth1"` - OAuth2 *garmin.OAuth2Token `json:"oauth2"` - }{ - OAuth1: garminClient.OAuth1Token(), - OAuth2: garminClient.OAuth2Token(), - } - - jsonBytes, err := json.MarshalIndent(tokens, "", " ") - if err != nil { - log.Fatalf("Failed to marshal tokens: %v", err) - } - fmt.Println(string(jsonBytes)) - }, -} - -func init() { - rootCmd.AddCommand(tokensCmd) -} diff --git a/cmd/garth/config.go b/cmd/garth/config.go deleted file mode 100644 index a11dcdb..0000000 --- a/cmd/garth/config.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "fmt" - "path/filepath" - - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - - "github.com/sstent/go-garth/internal/config" -) - -func init() { - rootCmd.AddCommand(configCmd) - configCmd.AddCommand(configInitCmd) - configCmd.AddCommand(configShowCmd) -} - -var configCmd = &cobra.Command{ - Use: "config", - Short: "Manage garth configuration", - Long: `Allows you to initialize, show, and manage garth's configuration file.`, -} - -var configInitCmd = &cobra.Command{ - Use: "init", - Short: "Initialize a default config file", - Long: `Creates a default garth configuration file in the standard location ($HOME/.config/garth/config.yaml) if one does not already exist.`, - RunE: func(cmd *cobra.Command, args []string) error { - configPath := filepath.Join(config.UserConfigDir(), "config.yaml") - _, err := config.InitConfig(configPath) - if err != nil { - return fmt.Errorf("error initializing config: %w", err) - } - fmt.Printf("Default config file initialized at: %s\n", configPath) - return nil - }, -} - -var configShowCmd = &cobra.Command{ - Use: "show", - Short: "Show the current configuration", - Long: `Displays the currently loaded garth configuration, including values from the config file and environment variables.`, - RunE: func(cmd *cobra.Command, args []string) error { - if cfg == nil { - return fmt.Errorf("configuration not loaded") - } - - data, err := yaml.Marshal(cfg) - if err != nil { - return fmt.Errorf("error marshaling config to YAML: %w", err) - } - fmt.Println(string(data)) - return nil - }, -} \ No newline at end of file diff --git a/cmd/garth/health.go b/cmd/garth/health.go deleted file mode 100644 index 1c7182f..0000000 --- a/cmd/garth/health.go +++ /dev/null @@ -1,911 +0,0 @@ -package main - -import ( - "encoding/csv" - "encoding/json" - "fmt" - "os" - "strconv" - "time" - - "github.com/rodaine/table" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/sstent/go-garth/internal/data" // Import the data package - types "github.com/sstent/go-garth/internal/models/types" - "github.com/sstent/go-garth/pkg/garmin" -) - -var ( - healthCmd = &cobra.Command{ - Use: "health", - Short: "Manage Garmin Connect health data", - Long: `Provides commands to fetch various health metrics like sleep, HRV, stress, and body battery.`, - } - - sleepCmd = &cobra.Command{ - Use: "sleep", - Short: "Get sleep data", - Long: `Fetch sleep data for a specified date range.`, - RunE: runSleep, - } - - hrvCmd = &cobra.Command{ - Use: "hrv", - Short: "Get HRV data", - Long: `Fetch Heart Rate Variability (HRV) data.`, - RunE: runHrv, - } - - stressCmd = &cobra.Command{ - Use: "stress", - Short: "Get stress data", - Long: `Fetch stress data.`, - RunE: runStress, - } - - bodyBatteryCmd = &cobra.Command{ - Use: "bodybattery", - Short: "Get Body Battery data", - Long: `Fetch Body Battery data.`, - RunE: runBodyBattery, - } - - 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, - } - - trainingStatusCmd = &cobra.Command{ - Use: "training-status", - Short: "Get Training Status data", - Long: `Fetch Training Status data.`, - RunE: runTrainingStatus, - } - - trainingLoadCmd = &cobra.Command{ - Use: "training-load", - Short: "Get Training Load data", - Long: `Fetch Training Load data.`, - RunE: runTrainingLoad, - } - - fitnessAgeCmd = &cobra.Command{ - Use: "fitness-age", - Short: "Get Fitness Age data", - Long: `Fetch Fitness Age data.`, - RunE: runFitnessAge, - } - - 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, - } - - healthDateFrom string - healthDateTo string - healthDays int - healthWeek bool - healthYesterday bool - healthAggregate string -) - -func init() { - rootCmd.AddCommand(healthCmd) - - healthCmd.AddCommand(sleepCmd) - 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(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)") - - healthCmd.AddCommand(hrvCmd) - 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) - 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) - bodyBatteryCmd.Flags().BoolVar(&healthYesterday, "yesterday", false, "Fetch data for yesterday") - bodyBatteryCmd.Flags().StringVar(&healthAggregate, "aggregate", "", "Aggregate data by (day, week, month, year)") - - 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)") - - healthCmd.AddCommand(hrZonesCmd) - - healthCmd.AddCommand(trainingStatusCmd) - trainingStatusCmd.Flags().StringVar(&healthDateFrom, "from", "", "Date for data fetching (YYYY-MM-DD, defaults to today)") - - healthCmd.AddCommand(trainingLoadCmd) - trainingLoadCmd.Flags().StringVar(&healthDateFrom, "from", "", "Date for data fetching (YYYY-MM-DD, defaults to today)") - - healthCmd.AddCommand(fitnessAgeCmd) - - 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 { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); 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, err = time.Parse("2006-01-02", healthDateTo) - if err != nil { - return fmt.Errorf("invalid date format for --to: %w", err) - } - } else { - endDate = time.Now() // Default to today - } - - var allSleepData []*data.DetailedSleepDataWithMethods - for d := startDate; !d.After(endDate); d = d.AddDate(0, 0, 1) { - // Create a new instance of DetailedSleepDataWithMethods for each day - sleepDataFetcher := &data.DetailedSleepDataWithMethods{} - sleepData, err := sleepDataFetcher.Get(d, garminClient.InternalClient()) - if err != nil { - return fmt.Errorf("failed to get sleep data for %s: %w", d.Format("2006-01-02"), err) - } - if sleepData != nil { - // Type assert the result back to DetailedSleepDataWithMethods - if sdm, ok := sleepData.(*data.DetailedSleepDataWithMethods); ok { - allSleepData = append(allSleepData, sdm) - } else { - return fmt.Errorf("unexpected type returned for sleep data: %T", sleepData) - } - } - } - - if len(allSleepData) == 0 { - fmt.Println("No sleep data found.") - return nil - } - - outputFormat := viper.GetString("output.format") - - switch outputFormat { - case "json": - data, err := json.MarshalIndent(allSleepData, "", " ") - 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", "TotalSleep", "Deep", "Light", "REM", "Awake", "AvgSpO2", "LowestSpO2", "AvgRespiration"}) - for _, data := range allSleepData { - writer.Write([]string{ - data.CalendarDate.Format("2006-01-02"), - fmt.Sprintf("%d", data.SleepScores.Overall), - (time.Duration(data.DeepSleepSeconds+data.LightSleepSeconds+data.RemSleepSeconds) * 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(), - func() string { - if data.AverageSpO2Value != nil { - return fmt.Sprintf("%.2f", *data.AverageSpO2Value) - } - return "N/A" - }(), - func() string { - if data.LowestSpO2Value != nil { - return fmt.Sprintf("%d", *data.LowestSpO2Value) - } - return "N/A" - }(), - func() string { - if data.AverageRespirationValue != nil { - return fmt.Sprintf("%.2f", *data.AverageRespirationValue) - } - return "N/A" - }(), - }) - } - case "table": - tbl := table.New("Date", "Score", "Total Sleep", "Deep", "Light", "REM", "Awake", "Avg SpO2", "Lowest SpO2", "Avg Resp") - for _, data := range allSleepData { - tbl.AddRow( - data.CalendarDate.Format("2006-01-02"), - fmt.Sprintf("%d", data.SleepScores.Overall), - (time.Duration(data.DeepSleepSeconds+data.LightSleepSeconds+data.RemSleepSeconds) * 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(), - func() string { - if data.AverageSpO2Value != nil { - return fmt.Sprintf("%.2f", *data.AverageSpO2Value) - } - return "N/A" - }(), - func() string { - if data.LowestSpO2Value != nil { - return fmt.Sprintf("%d", *data.LowestSpO2Value) - } - return "N/A" - }(), - func() string { - if data.AverageRespirationValue != nil { - return fmt.Sprintf("%.2f", *data.AverageRespirationValue) - } - return "N/A" - }(), - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runHrv(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - days := healthDays - if days == 0 { - days = 7 // Default to 7 days if not specified - } - - var allHrvData []*data.DailyHRVDataWithMethods - for d := time.Now().AddDate(0, 0, -days+1); !d.After(time.Now()); d = d.AddDate(0, 0, 1) { - hrvDataFetcher := &data.DailyHRVDataWithMethods{} - hrvData, err := hrvDataFetcher.Get(d, garminClient.InternalClient()) - if err != nil { - return fmt.Errorf("failed to get HRV data for %s: %w", d.Format("2006-01-02"), err) - } - if hrvData != nil { - if hdm, ok := hrvData.(*data.DailyHRVDataWithMethods); ok { - allHrvData = append(allHrvData, hdm) - } else { - return fmt.Errorf("unexpected type returned for HRV data: %T", hrvData) - } - } - } - - if len(allHrvData) == 0 { - fmt.Println("No HRV data found.") - return nil - } - - outputFormat := viper.GetString("output.format") - - switch outputFormat { - case "json": - data, err := json.MarshalIndent(allHrvData, "", " ") - 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", "WeeklyAvg", "LastNightAvg", "Status", "Feedback"}) - for _, data := range allHrvData { - writer.Write([]string{ - data.CalendarDate.Format("2006-01-02"), - func() string { - if data.WeeklyAvg != nil { - return fmt.Sprintf("%.2f", *data.WeeklyAvg) - } - return "N/A" - }(), - func() string { - if data.LastNightAvg != nil { - return fmt.Sprintf("%.2f", *data.LastNightAvg) - } - return "N/A" - }(), - data.Status, - data.FeedbackPhrase, - }) - } - case "table": - tbl := table.New("Date", "Weekly Avg", "Last Night Avg", "Status", "Feedback") - for _, data := range allHrvData { - tbl.AddRow( - data.CalendarDate.Format("2006-01-02"), - func() string { - if data.WeeklyAvg != nil { - return fmt.Sprintf("%.2f", *data.WeeklyAvg) - } - return "N/A" - }(), - func() string { - if data.LastNightAvg != nil { - return fmt.Sprintf("%.2f", *data.LastNightAvg) - } - return "N/A" - }(), - data.Status, - data.FeedbackPhrase, - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runStress(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - var startDate, endDate time.Time - if healthWeek { - now := time.Now() - weekday := now.Weekday() - // Calculate the start of the current week (Sunday) - startDate = now.AddDate(0, 0, -int(weekday)) - endDate = startDate.AddDate(0, 0, 6) // End of the current week (Saturday) - } else { - // Default to today if no specific range or week is given - startDate = time.Now() - endDate = time.Now() - } - - stressData, err := garminClient.GetStressData(startDate, endDate) - if err != nil { - return fmt.Errorf("failed to get stress data: %w", err) - } - - if len(stressData) == 0 { - fmt.Println("No stress data found.") - return nil - } - - // Apply aggregation if requested - if healthAggregate != "" { - aggregatedStress := make(map[string]struct { - 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 = []types.StressData{} - for key, entry := range aggregatedStress { - stressData = append(stressData, types.StressData{ - Date: types.ParseAggregationKey(key, healthAggregate), - 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": - tbl := table.New("Date", "Stress Level", "Rest Stress Level") - for _, data := range stressData { - tbl.AddRow( - data.Date.Format("2006-01-02"), - fmt.Sprintf("%d", data.StressLevel), - fmt.Sprintf("%d", data.RestStressLevel), - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runBodyBattery(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - var targetDate time.Time - if healthYesterday { - targetDate = time.Now().AddDate(0, 0, -1) - } else { - targetDate = time.Now() - } - - bodyBatteryDataFetcher := &data.BodyBatteryDataWithMethods{} - result, err := bodyBatteryDataFetcher.Get(targetDate, garminClient.InternalClient()) - if err != nil { - return fmt.Errorf("failed to get Body Battery data: %w", err) - } - bodyBatteryData, ok := result.(*data.BodyBatteryDataWithMethods) - if !ok { - return fmt.Errorf("unexpected type for Body Battery data: %T", result) - } - - if bodyBatteryData == nil { - fmt.Println("No Body Battery data found.") - return nil - } - - outputFormat := viper.GetString("output.format") - - 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", "CurrentLevel", "DayChange", "MaxStressLevel", "AvgStressLevel"}) - writer.Write([]string{ - bodyBatteryData.CalendarDate.Format("2006-01-02"), - fmt.Sprintf("%d", bodyBatteryData.GetCurrentLevel()), - fmt.Sprintf("%d", bodyBatteryData.GetDayChange()), - fmt.Sprintf("%d", bodyBatteryData.MaxStressLevel), - fmt.Sprintf("%d", bodyBatteryData.AvgStressLevel), - }) - case "table": - tbl := table.New("Date", "Current Level", "Day Change", "Max Stress", "Avg Stress") - tbl.AddRow( - bodyBatteryData.CalendarDate.Format("2006-01-02"), - fmt.Sprintf("%d", bodyBatteryData.GetCurrentLevel()), - fmt.Sprintf("%d", bodyBatteryData.GetDayChange()), - fmt.Sprintf("%d", bodyBatteryData.MaxStressLevel), - fmt.Sprintf("%d", bodyBatteryData.AvgStressLevel), - ) - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runVO2Max(cmd *cobra.Command, args []string) error { - client, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := client.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - profile, err := client.InternalClient().GetCurrentVO2Max() - if err != nil { - return fmt.Errorf("failed to get VO2 Max data: %w", err) - } - - if profile.Running == nil && profile.Cycling == nil { - fmt.Println("No VO2 Max data found.") - return nil - } - - outputFormat := viper.GetString("output.format") - - switch outputFormat { - case "json": - data, err := json.MarshalIndent(profile, "", " ") - 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{"Type", "Value", "Date", "Source"}) - if profile.Running != nil { - writer.Write([]string{ - profile.Running.ActivityType, - fmt.Sprintf("%.2f", profile.Running.Value), - profile.Running.Date.Format("2006-01-02"), - profile.Running.Source, - }) - } - if profile.Cycling != nil { - writer.Write([]string{ - profile.Cycling.ActivityType, - fmt.Sprintf("%.2f", profile.Cycling.Value), - profile.Cycling.Date.Format("2006-01-02"), - profile.Cycling.Source, - }) - } - case "table": - tbl := table.New("Type", "Value", "Date", "Source") - - if profile.Running != nil { - tbl.AddRow( - profile.Running.ActivityType, - fmt.Sprintf("%.2f", profile.Running.Value), - profile.Running.Date.Format("2006-01-02"), - profile.Running.Source, - ) - } - if profile.Cycling != nil { - tbl.AddRow( - profile.Cycling.ActivityType, - fmt.Sprintf("%.2f", profile.Cycling.Value), - fmt.Sprintf("%.2f", profile.Cycling.Value), - profile.Cycling.Date.Format("2006-01-02"), - profile.Cycling.Source, - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runHRZones(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); 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": - tbl := table.New("Resting HR", "Max HR", "Lactate Threshold", "Updated At") - tbl.AddRow( - strconv.Itoa(hrZonesData.RestingHR), - strconv.Itoa(hrZonesData.MaxHR), - strconv.Itoa(hrZonesData.LactateThreshold), - hrZonesData.UpdatedAt.Format("2006-01-02 15:04:05"), - ) - tbl.Print() - - fmt.Println() - - zonesTable := table.New("Zone", "Min BPM", "Max BPM", "Name") - for _, zone := range hrZonesData.Zones { - zonesTable.AddRow( - strconv.Itoa(zone.Zone), - strconv.Itoa(zone.MinBPM), - strconv.Itoa(zone.MaxBPM), - zone.Name, - ) - } - zonesTable.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runWellness(cmd *cobra.Command, args []string) error { - return fmt.Errorf("not implemented") -} - -func runTrainingStatus(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - var targetDate time.Time - if healthDateFrom != "" { - targetDate, err = time.Parse("2006-01-02", healthDateFrom) - if err != nil { - return fmt.Errorf("invalid date format for --from: %w", err) - } - } else { - targetDate = time.Now() - } - - trainingStatusFetcher := &data.TrainingStatusWithMethods{} - trainingStatus, err := trainingStatusFetcher.Get(targetDate, garminClient.InternalClient()) - if err != nil { - return fmt.Errorf("failed to get training status: %w", err) - } - - if trainingStatus == nil { - fmt.Println("No training status data found.") - return nil - } - - tsm, ok := trainingStatus.(*data.TrainingStatusWithMethods) - if !ok { - return fmt.Errorf("unexpected type returned for training status: %T", trainingStatus) - } - - outputFormat := viper.GetString("output.format") - - switch outputFormat { - case "json": - data, err := json.MarshalIndent(tsm, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal training status to JSON: %w", err) - } - fmt.Println(string(data)) - case "csv": - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - writer.Write([]string{"Date", "Status", "LoadRatio"}) - writer.Write([]string{ - tsm.CalendarDate.Format("2006-01-02"), - tsm.TrainingStatusKey, - fmt.Sprintf("%.2f", tsm.LoadRatio), - }) - case "table": - tbl := table.New("Date", "Status", "Load Ratio") - tbl.AddRow( - tsm.CalendarDate.Format("2006-01-02"), - tsm.TrainingStatusKey, - fmt.Sprintf("%.2f", tsm.LoadRatio), - ) - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runTrainingLoad(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - var targetDate time.Time - if healthDateFrom != "" { - targetDate, err = time.Parse("2006-01-02", healthDateFrom) - if err != nil { - return fmt.Errorf("invalid date format for --from: %w", err) - } - } else { - targetDate = time.Now() - } - - trainingLoadFetcher := &data.TrainingLoadWithMethods{} - trainingLoad, err := trainingLoadFetcher.Get(targetDate, garminClient.InternalClient()) - if err != nil { - return fmt.Errorf("failed to get training load: %w", err) - } - - if trainingLoad == nil { - fmt.Println("No training load data found.") - return nil - } - - tlm, ok := trainingLoad.(*data.TrainingLoadWithMethods) - if !ok { - return fmt.Errorf("unexpected type returned for training load: %T", trainingLoad) - } - - outputFormat := viper.GetString("output.format") - - switch outputFormat { - case "json": - data, err := json.MarshalIndent(tlm, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal training load to JSON: %w", err) - } - fmt.Println(string(data)) - case "csv": - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - writer.Write([]string{"Date", "AcuteLoad", "ChronicLoad", "LoadRatio"}) - writer.Write([]string{ - tlm.CalendarDate.Format("2006-01-02"), - fmt.Sprintf("%.2f", tlm.AcuteTrainingLoad), - fmt.Sprintf("%.2f", tlm.ChronicTrainingLoad), - fmt.Sprintf("%.2f", tlm.TrainingLoadRatio), - }) - case "table": - tbl := table.New("Date", "Acute Load", "Chronic Load", "Load Ratio") - tbl.AddRow( - tlm.CalendarDate.Format("2006-01-02"), - fmt.Sprintf("%.2f", tlm.AcuteTrainingLoad), - fmt.Sprintf("%.2f", tlm.ChronicTrainingLoad), - fmt.Sprintf("%.2f", tlm.TrainingLoadRatio), - ) - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runFitnessAge(cmd *cobra.Command, args []string) error { - garminClient, err := garmin.NewClient(viper.GetString("domain")) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - if err := garminClient.LoadSession(viper.GetString("session_file")); err != nil { - return fmt.Errorf("not logged in: %w", err) - } - - fitnessAge, err := garminClient.GetFitnessAge() - if err != nil { - return fmt.Errorf("failed to get fitness age: %w", err) - } - - if fitnessAge == nil { - fmt.Println("No fitness age data found.") - return nil - } - - outputFormat := viper.GetString("output.format") - - switch outputFormat { - case "json": - data, err := json.MarshalIndent(fitnessAge, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal fitness age to JSON: %w", err) - } - fmt.Println(string(data)) - case "csv": - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - writer.Write([]string{"FitnessAge", "ChronologicalAge", "VO2MaxRunning", "LastUpdated"}) - writer.Write([]string{ - fmt.Sprintf("%d", fitnessAge.FitnessAge), - fmt.Sprintf("%d", fitnessAge.ChronologicalAge), - fmt.Sprintf("%.2f", fitnessAge.VO2MaxRunning), - fitnessAge.LastUpdated.Format("2006-01-02"), - }) - case "table": - tbl := table.New("Fitness Age", "Chronological Age", "VO2 Max Running", "Last Updated") - tbl.AddRow( - fmt.Sprintf("%d", fitnessAge.FitnessAge), - fmt.Sprintf("%d", fitnessAge.ChronologicalAge), - fmt.Sprintf("%.2f", fitnessAge.VO2MaxRunning), - fitnessAge.LastUpdated.Format("2006-01-02"), - ) - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} diff --git a/cmd/garth/main.go b/cmd/garth/main.go deleted file mode 100644 index 736ef31..0000000 --- a/cmd/garth/main.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -func main() { - Execute() -} diff --git a/cmd/garth/root.go b/cmd/garth/root.go deleted file mode 100644 index 446758a..0000000 --- a/cmd/garth/root.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/sstent/go-garth/internal/config" -) - -var ( - cfgFile string - userConfigDir string - cfg *config.Config -) - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "garth", - Short: "Garmin Connect CLI tool", - Long: `A comprehensive CLI tool for interacting with Garmin Connect. - -Garth allows you to fetch your Garmin Connect data, including activities, -health stats, and more, directly from your terminal.`, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // Ensure config is loaded before any command runs - if cfg == nil { - return fmt.Errorf("configuration not loaded") - } - return nil - }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - // Global flags - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/garth/config.yaml)") - rootCmd.PersistentFlags().StringVar(&userConfigDir, "config-dir", "", "config directory (default is $HOME/.config/garth)") - - rootCmd.PersistentFlags().String("output", "table", "output format (json, table, csv)") - rootCmd.PersistentFlags().Bool("verbose", false, "enable verbose output") - rootCmd.PersistentFlags().String("date-from", "", "start date for data fetching (YYYY-MM-DD)") - rootCmd.PersistentFlags().String("date-to", "", "end date for data fetching (YYYY-MM-DD)") - - // Bind flags to viper - _ = viper.BindPFlag("output.format", rootCmd.PersistentFlags().Lookup("output")) - _ = viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) - _ = viper.BindPFlag("dateFrom", rootCmd.PersistentFlags().Lookup("date-from")) - _ = viper.BindPFlag("dateTo", rootCmd.PersistentFlags().Lookup("date-to")) -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if userConfigDir == "" { - userConfigDir = config.UserConfigDir() - } - - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Search config in user's config directory with name "config" (without extension). - viper.AddConfigPath(userConfigDir) - viper.SetConfigName("config") - viper.SetConfigType("yaml") - } - - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) - } else { - // If config file not found, try to initialize a default one - defaultConfigPath := filepath.Join(userConfigDir, "config.yaml") - if _, statErr := os.Stat(defaultConfigPath); os.IsNotExist(statErr) { - fmt.Fprintln(os.Stderr, "No config file found. Initializing default config at:", defaultConfigPath) - var initErr error - cfg, initErr = config.InitConfig(defaultConfigPath) - if initErr != nil { - fmt.Fprintln(os.Stderr, "Error initializing default config:", initErr) - os.Exit(1) - } - } else if statErr != nil { - fmt.Fprintln(os.Stderr, "Error checking for config file:", statErr) - os.Exit(1) - } - } - - // Unmarshal config into our struct - if cfg == nil { // Only unmarshal if not already initialized by InitConfig - cfg = config.DefaultConfig() // Start with defaults - if err := viper.Unmarshal(cfg); err != nil { - fmt.Fprintln(os.Stderr, "Error unmarshaling config:", err) - os.Exit(1) - } - } - - // Override config with flag values - if rootCmd.PersistentFlags().Lookup("output").Changed { - cfg.Output.Format = viper.GetString("output.format") - } - // Add other flag overrides as needed -} diff --git a/cmd/garth/stats.go b/cmd/garth/stats.go deleted file mode 100644 index 32685e6..0000000 --- a/cmd/garth/stats.go +++ /dev/null @@ -1,238 +0,0 @@ -package main - -import ( - "encoding/csv" - "encoding/json" - "fmt" - "os" - "time" - - "github.com/rodaine/table" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - types "github.com/sstent/go-garth/internal/models/types" - "github.com/sstent/go-garth/pkg/garmin" -) - -var ( - statsYear bool - statsAggregate string - statsFrom string -) - -func runDistance(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 statsYear { - now := time.Now() - startDate = time.Date(now.Year(), time.January, 1, 0, 0, 0, 0, now.Location()) - endDate = time.Date(now.Year(), time.December, 31, 0, 0, 0, 0, now.Location()) // Last day of the year - } else { - // Default to today if no specific range or year is given - startDate = time.Now() - endDate = time.Now() - } - - distanceData, err := garminClient.GetDistanceData(startDate, endDate) - if err != nil { - return fmt.Errorf("failed to get distance data: %w", err) - } - - if len(distanceData) == 0 { - fmt.Println("No distance data found.") - return nil - } - - // Apply aggregation if requested - if statsAggregate != "" { - 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 = []types.DistanceData{} - for key, entry := range aggregatedDistance { - distanceData = append(distanceData, types.DistanceData{ - Date: types.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": - tbl := table.New("Date", "Distance (km)") - for _, data := range distanceData { - tbl.AddRow( - data.Date.Format("2006-01-02"), - fmt.Sprintf("%.2f", data.Distance/1000), - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} - -func runCalories(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 statsFrom != "" { - startDate, err = time.Parse("2006-01-02", statsFrom) - if err != nil { - return fmt.Errorf("invalid date format for --from: %w", err) - } - endDate = time.Now() // Default end date to today if only from is provided - } else { - // Default to today if no specific range is given - startDate = time.Now() - endDate = time.Now() - } - - caloriesData, err := garminClient.GetCaloriesData(startDate, endDate) - if err != nil { - return fmt.Errorf("failed to get calories data: %w", err) - } - - if len(caloriesData) == 0 { - fmt.Println("No calories data found.") - return nil - } - - // Apply aggregation if requested - if statsAggregate != "" { - 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 = []types.CaloriesData{} - for key, entry := range aggregatedCalories { - caloriesData = append(caloriesData, types.CaloriesData{ - Date: types.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": - tbl := table.New("Date", "Calories") - for _, data := range caloriesData { - tbl.AddRow( - data.Date.Format("2006-01-02"), - fmt.Sprintf("%d", data.Calories), - ) - } - tbl.Print() - default: - return fmt.Errorf("unsupported output format: %s", outputFormat) - } - - return nil -} diff --git a/e2e_test.sh b/e2e_test.sh deleted file mode 100755 index eb21ed6..0000000 --- a/e2e_test.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -set -e - -echo "--- Running End-to-End CLI Tests ---" - -echo "Testing garth --help" -go run go-garth/cmd/garth --help - -echo "Testing garth auth status" -go run go-garth/cmd/garth auth status - -echo "Testing garth activities list" -go run go-garth/cmd/garth activities list --limit 5 - -echo "Testing garth health sleep" -go run go-garth/cmd/garth health sleep --from 2024-01-01 --to 2024-01-02 - -echo "Testing garth stats distance" -go run go-garth/cmd/garth stats distance --year - -echo "Testing garth health vo2max" -go run go-garth/cmd/garth health vo2max --from 2024-01-01 --to 2024-01-02 - -echo "Testing garth health hr-zones" -go run go-garth/cmd/garth health hr-zones - -echo "--- End-to-End CLI Tests Passed ---" \ No newline at end of file diff --git a/garth b/garth deleted file mode 100755 index 069a613..0000000 Binary files a/garth and /dev/null differ diff --git a/go.mod b/go.mod index a62f011..c5ff17b 100644 --- a/go.mod +++ b/go.mod @@ -2,31 +2,11 @@ module github.com/sstent/go-garth go 1.24.2 -require ( - github.com/joho/godotenv v1.5.1 - github.com/rodaine/table v1.3.0 - github.com/schollz/progressbar/v3 v3.18.0 - github.com/spf13/cobra v1.10.1 - github.com/spf13/viper v1.21.0 - golang.org/x/term v0.28.0 -) +require github.com/joho/godotenv v1.5.1 require ( - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.28.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) require ( diff --git a/go.sum b/go.sum index ae478a6..3e7493f 100644 --- a/go.sum +++ b/go.sum @@ -1,81 +1,21 @@ -github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= -github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -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/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= -github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= -github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -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/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/auth/oauth/oauth.go b/pkg/auth/oauth/oauth.go new file mode 100644 index 0000000..f35f99c --- /dev/null +++ b/pkg/auth/oauth/oauth.go @@ -0,0 +1,25 @@ +package oauth + +import ( + "github.com/sstent/go-garth/internal/auth/oauth" + "github.com/sstent/go-garth/internal/models/types" + "github.com/sstent/go-garth/pkg/garmin" +) + +// GetOAuth1Token retrieves an OAuth1 token using the provided ticket +func GetOAuth1Token(domain, ticket string) (*garmin.OAuth1Token, error) { + token, err := oauth.GetOAuth1Token(domain, ticket) + if err != nil { + return nil, err + } + return (*garmin.OAuth1Token)(token), nil +} + +// ExchangeToken exchanges an OAuth1 token for an OAuth2 token +func ExchangeToken(oauth1Token *garmin.OAuth1Token) (*garmin.OAuth2Token, error) { + token, err := oauth.ExchangeToken((*types.OAuth1Token)(oauth1Token)) + if err != nil { + return nil, err + } + return (*garmin.OAuth2Token)(token), nil +} diff --git a/python-garmin-connect/Activity.go b/python-garmin-connect/Activity.go deleted file mode 100644 index 7f4945a..0000000 --- a/python-garmin-connect/Activity.go +++ /dev/null @@ -1,229 +0,0 @@ -package connect - -import ( - "archive/zip" - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "strings" -) - -// Activity describes a Garmin Connect activity. -type Activity struct { - ID int `json:"activityId"` - ActivityName string `json:"activityName"` - Description string `json:"description"` - StartLocal Time `json:"startTimeLocal"` - StartGMT Time `json:"startTimeGMT"` - ActivityType ActivityType `json:"activityType"` - Distance float64 `json:"distance"` // meter - Duration float64 `json:"duration"` - ElapsedDuration float64 `json:"elapsedDuration"` - MovingDuration float64 `json:"movingDuration"` - AverageSpeed float64 `json:"averageSpeed"` - MaxSpeed float64 `json:"maxSpeed"` - OwnerID int `json:"ownerId"` - Calories float64 `json:"calories"` - AverageHeartRate float64 `json:"averageHR"` - MaxHeartRate float64 `json:"maxHR"` - DeviceID int `json:"deviceId"` -} - -// ActivityType describes the type of activity. -type ActivityType struct { - TypeID int `json:"typeId"` - TypeKey string `json:"typeKey"` - ParentTypeID int `json:"parentTypeId"` - SortOrder int `json:"sortOrder"` -} - -// Activity will retrieve details about an activity. -func (c *Client) Activity(activityID int) (*Activity, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/activity-service/activity/%d", - activityID, - ) - - activity := new(Activity) - - err := c.getJSON(URL, &activity) - if err != nil { - return nil, err - } - - return activity, nil -} - -// Activities will list activities for displayName. If displayName is empty, -// the authenticated user will be used. -func (c *Client) Activities(displayName string, start int, limit int) ([]Activity, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/activitylist-service/activities/%s?start=%d&limit=%d", displayName, start, limit) - - if !c.authenticated() && displayName == "" { - return nil, ErrNotAuthenticated - } - - var proxy struct { - List []Activity `json:"activityList"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - return proxy.List, nil -} - -// RenameActivity can be used to rename an activity. -func (c *Client) RenameActivity(activityID int, newName string) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/activity-service/activity/%d", activityID) - - payload := struct { - ID int `json:"activityId"` - Name string `json:"activityName"` - }{activityID, newName} - - return c.write("PUT", URL, payload, 204) -} - -// ExportActivity will export an activity from Connect. The activity will be written til w. -func (c *Client) ExportActivity(id int, w io.Writer, format ActivityFormat) error { - formatTable := [activityFormatMax]string{ - "https://connect.garmin.com/modern/proxy/download-service/files/activity/%d", - "https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/%d", - "https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/%d", - "https://connect.garmin.com/modern/proxy/download-service/export/kml/activity/%d", - "https://connect.garmin.com/modern/proxy/download-service/export/csv/activity/%d", - } - - if format >= activityFormatMax || format < ActivityFormatFIT { - return errors.New("invalid format") - } - - URL := fmt.Sprintf(formatTable[format], id) - - // To unzip FIT files on-the-fly, we treat them specially. - if format == ActivityFormatFIT { - buffer := bytes.NewBuffer(nil) - - err := c.Download(URL, buffer) - if err != nil { - return err - } - - z, err := zip.NewReader(bytes.NewReader(buffer.Bytes()), int64(buffer.Len())) - if err != nil { - return err - } - - if len(z.File) != 1 { - return fmt.Errorf("%d files found in FIT archive, 1 expected", len(z.File)) - } - - src, err := z.File[0].Open() - if err != nil { - return err - } - defer src.Close() - - _, err = io.Copy(w, src) - return err - } - - return c.Download(URL, w) -} - -// ImportActivity will import an activity into Garmin Connect. The activity -// will be read from file. -func (c *Client) ImportActivity(file io.Reader, format ActivityFormat) (int, error) { - URL := "https://connect.garmin.com/modern/proxy/upload-service/upload/." + format.Extension() - - switch format { - case ActivityFormatFIT, ActivityFormatTCX, ActivityFormatGPX: - // These are ok. - default: - return 0, fmt.Errorf("%s is not supported for import", format.Extension()) - } - - formData := bytes.Buffer{} - writer := multipart.NewWriter(&formData) - defer writer.Close() - - activity, err := writer.CreateFormFile("file", "activity."+format.Extension()) - if err != nil { - return 0, err - } - - _, err = io.Copy(activity, file) - if err != nil { - return 0, err - } - - writer.Close() - - req, err := c.newRequest("POST", URL, &formData) - if err != nil { - return 0, err - } - - req.Header.Add("content-type", writer.FormDataContentType()) - - resp, err := c.do(req) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - // Implement enough of the response to satisfy our needs. - var response struct { - ImportResult struct { - Successes []struct { - InternalID int `json:"internalId"` - } `json:"successes"` - - Failures []struct { - Messages []struct { - Content string `json:"content"` - } `json:"messages"` - } `json:"failures"` - } `json:"detailedImportResult"` - } - - err = json.NewDecoder(resp.Body).Decode(&response) - if err != nil { - return 0, err - } - - // This is ugly. - if len(response.ImportResult.Failures) > 0 { - messages := make([]string, 0, 10) - for _, f := range response.ImportResult.Failures { - for _, m := range f.Messages { - messages = append(messages, m.Content) - } - } - - return 0, errors.New(strings.Join(messages, "; ")) - } - - if resp.StatusCode != 201 { - return 0, fmt.Errorf("%d: %s", resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - if len(response.ImportResult.Successes) != 1 { - return 0, Error("cannot parse response, no failures and no successes..?") - } - - return response.ImportResult.Successes[0].InternalID, nil -} - -// DeleteActivity will permanently delete an activity. -func (c *Client) DeleteActivity(id int) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/activity-service/activity/%d", id) - - return c.write("DELETE", URL, nil, 0) -} diff --git a/python-garmin-connect/ActivityFormat.go b/python-garmin-connect/ActivityFormat.go deleted file mode 100644 index f4b0af9..0000000 --- a/python-garmin-connect/ActivityFormat.go +++ /dev/null @@ -1,75 +0,0 @@ -package connect - -import ( - "path/filepath" - "strings" -) - -// ActivityFormat is a file format for importing and exporting activities. -type ActivityFormat int - -const ( - // ActivityFormatFIT is the "original" Garmin format. - ActivityFormatFIT ActivityFormat = iota - - // ActivityFormatTCX is Training Center XML (TCX) format. - ActivityFormatTCX - - // ActivityFormatGPX will export as GPX - the GPS Exchange Format. - ActivityFormatGPX - - // ActivityFormatKML will export KML files compatible with Google Earth. - ActivityFormatKML - - // ActivityFormatCSV will export splits as CSV. - ActivityFormatCSV - - activityFormatMax - activityFormatInvalid -) - -const ( - // ErrUnknownFormat will be returned if the activity file format is unknown. - ErrUnknownFormat = Error("Unknown format") -) - -var ( - activityFormatTable = map[string]ActivityFormat{ - "fit": ActivityFormatFIT, - "tcx": ActivityFormatTCX, - "gpx": ActivityFormatGPX, - "kml": ActivityFormatKML, - "csv": ActivityFormatCSV, - } -) - -// Extension returns an appropriate filename extension for format. -func (f ActivityFormat) Extension() string { - for extension, format := range activityFormatTable { - if format == f { - return extension - } - } - - return "" -} - -// FormatFromExtension tries to guess the format from a file extension. -func FormatFromExtension(extension string) (ActivityFormat, error) { - extension = strings.ToLower(extension) - - format, found := activityFormatTable[extension] - if !found { - return activityFormatInvalid, ErrUnknownFormat - } - - return format, nil -} - -// FormatFromFilename tries to guess the format based on a filename (or path). -func FormatFromFilename(filename string) (ActivityFormat, error) { - extension := filepath.Ext(filename) - extension = strings.TrimPrefix(extension, ".") - - return FormatFromExtension(extension) -} diff --git a/python-garmin-connect/ActivityHrZones.go b/python-garmin-connect/ActivityHrZones.go deleted file mode 100644 index a7246c1..0000000 --- a/python-garmin-connect/ActivityHrZones.go +++ /dev/null @@ -1,41 +0,0 @@ -package connect - -import ( - "fmt" - "time" -) - -// ActivityHrZones describes the heart-rate zones during an activity. -type ActivityHrZones struct { - TimeInZone time.Duration `json:"secsInZone"` - ZoneLowBoundary int `json:"zoneLowBoundary"` - ZoneNumber int `json:"zoneNumber"` -} - -// ActivityHrZones returns the reported heart-rate zones for an activity. -func (c *Client) ActivityHrZones(activityID int) ([]ActivityHrZones, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/activity-service/activity/%d/hrTimeInZones", - activityID, - ) - - var proxy []struct { - TimeInZone float64 `json:"secsInZone"` - ZoneLowBoundary int `json:"zoneLowBoundary"` - ZoneNumber int `json:"zoneNumber"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - zones := make([]ActivityHrZones, len(proxy)) - - for i, p := range proxy { - zones[i].TimeInZone = time.Duration(p.TimeInZone * float64(time.Second)) - zones[i].ZoneLowBoundary = p.ZoneLowBoundary - zones[i].ZoneNumber = p.ZoneNumber - } - - return zones, nil -} diff --git a/python-garmin-connect/ActivityWeather.go b/python-garmin-connect/ActivityWeather.go deleted file mode 100644 index 9fca035..0000000 --- a/python-garmin-connect/ActivityWeather.go +++ /dev/null @@ -1,34 +0,0 @@ -package connect - -import ( - "fmt" -) - -// ActivityWeather describes the weather during an activity. -type ActivityWeather struct { - Temperature int `json:"temp"` - ApparentTemperature int `json:"apparentTemp"` - DewPoint int `json:"dewPoint"` - RelativeHumidity int `json:"relativeHumidity"` - WindDirection int `json:"windDirection"` - WindDirectionCompassPoint string `json:"windDirectionCompassPoint"` - WindSpeed int `json:"windSpeed"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` -} - -// ActivityWeather returns the reported weather for an activity. -func (c *Client) ActivityWeather(activityID int) (*ActivityWeather, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/weather-service/weather/%d", - activityID, - ) - - weather := new(ActivityWeather) - - err := c.getJSON(URL, weather) - if err != nil { - return nil, err - } - - return weather, nil -} diff --git a/python-garmin-connect/AdhocChallenge.go b/python-garmin-connect/AdhocChallenge.go deleted file mode 100644 index 2e57b2a..0000000 --- a/python-garmin-connect/AdhocChallenge.go +++ /dev/null @@ -1,108 +0,0 @@ -package connect - -import ( - "fmt" -) - -// Player represents a participant in a challenge. -type Player struct { - UserProfileID int `json:"userProfileId"` - TotalNumber float64 `json:"totalNumber"` - LastSyncTime Time `json:"lastSyncTime"` - Ranking int `json:"ranking"` - ProfileImageURLSmall string `json:"profileImageSmall"` - ProfileImageURLMedium string `json:"profileImageMedium"` - FullName string `json:"fullName"` - DisplayName string `json:"displayName"` - ProUser bool `json:"isProUser"` - TodayNumber float64 `json:"todayNumber"` - AcceptedChallenge bool `json:"isAcceptedChallenge"` -} - -// AdhocChallenge is a user-initiated challenge between 2 or more participants. -type AdhocChallenge struct { - SocialChallengeStatusID int `json:"socialChallengeStatusId"` - SocialChallengeActivityTypeID int `json:"socialChallengeActivityTypeId"` - SocialChallengeType int `json:"socialChallengeType"` - Name string `json:"adHocChallengeName"` - Description string `json:"adHocChallengeDesc"` - OwnerProfileID int `json:"ownerUserProfileId"` - UUID string `json:"uuid"` - Start Time `json:"startDate"` - End Time `json:"endDate"` - DurationTypeID int `json:"durationTypeId"` - UserRanking int `json:"userRanking"` - Players []Player `json:"players"` -} - -// AdhocChallenges will list the currently non-completed Ad-Hoc challenges. -// Please note that Players will not be populated, use AdhocChallenge() to -// retrieve players for a challenge. -func (c *Client) AdhocChallenges() ([]AdhocChallenge, error) { - URL := "https://connect.garmin.com/modern/proxy/adhocchallenge-service/adHocChallenge/nonCompleted" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - challenges := make([]AdhocChallenge, 0, 10) - - err := c.getJSON(URL, &challenges) - if err != nil { - return nil, err - } - - return challenges, nil -} - -// HistoricalAdhocChallenges will retrieve the list of completed ad-hoc -// challenges. -func (c *Client) HistoricalAdhocChallenges() ([]AdhocChallenge, error) { - URL := "https://connect.garmin.com/modern/proxy/adhocchallenge-service/adHocChallenge/historical" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - challenges := make([]AdhocChallenge, 0, 100) - - err := c.getJSON(URL, &challenges) - if err != nil { - return nil, err - } - - return challenges, nil -} - -// AdhocChallenge will retrieve details for challenge with uuid. -func (c *Client) AdhocChallenge(uuid string) (*AdhocChallenge, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/adhocchallenge-service/adHocChallenge/%s", uuid) - - challenge := new(AdhocChallenge) - - err := c.getJSON(URL, challenge) - if err != nil { - return nil, err - } - - return challenge, nil -} - -// LeaveAdhocChallenge will leave an ad-hoc challenge. If profileID is 0, the -// currently authenticated user will be used. -func (c *Client) LeaveAdhocChallenge(challengeUUID string, profileID int64) error { - if profileID == 0 && c.Profile == nil { - return ErrNotAuthenticated - } - - if profileID == 0 && c.Profile != nil { - profileID = c.Profile.ProfileID - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/adhocchallenge-service/adHocChallenge/%s/player/%d", - challengeUUID, - profileID, - ) - - return c.write("DELETE", URL, nil, 0) -} diff --git a/python-garmin-connect/AdhocChallengeInvitation.go b/python-garmin-connect/AdhocChallengeInvitation.go deleted file mode 100644 index 2805996..0000000 --- a/python-garmin-connect/AdhocChallengeInvitation.go +++ /dev/null @@ -1,63 +0,0 @@ -package connect - -import ( - "fmt" -) - -// AdhocChallengeInvitation is a ad-hoc challenge invitation. -type AdhocChallengeInvitation struct { - AdhocChallenge `json:",inline"` - - UUID string `json:"adHocChallengeUuid"` - InviteID int `json:"adHocChallengeInviteId"` - InvitorName string `json:"invitorName"` - InvitorID int `json:"invitorId"` - InvitorDisplayName string `json:"invitorDisplayName"` - InviteeID int `json:"inviteeId"` - UserImageURL string `json:"userImageUrl"` -} - -// AdhocChallengeInvites list Ad-Hoc challenges awaiting response. -func (c *Client) AdhocChallengeInvites() ([]AdhocChallengeInvitation, error) { - URL := "https://connect.garmin.com/modern/proxy/adhocchallenge-service/adHocChallenge/invite" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - challenges := make([]AdhocChallengeInvitation, 0, 10) - - err := c.getJSON(URL, &challenges) - if err != nil { - return nil, err - } - - // Make sure the embedded UUID matches in case the user uses the embedded - // AdhocChallenge for something. - for i := range challenges { - challenges[i].AdhocChallenge.UUID = challenges[i].UUID - } - - return challenges, nil -} - -// AdhocChallengeInvitationRespond will respond to a ad-hoc challenge. If -// accept is false, the challenge will be declined. -func (c *Client) AdhocChallengeInvitationRespond(inviteID int, accept bool) error { - scope := "decline" - if accept { - scope = "accept" - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/adhocchallenge-service/adHocChallenge/invite/%d/%s", inviteID, scope) - - payload := struct { - InviteID int `json:"inviteId"` - Scope string `json:"scope"` - }{ - inviteID, - scope, - } - - return c.write("PUT", URL, payload, 0) -} diff --git a/python-garmin-connect/Badge.go b/python-garmin-connect/Badge.go deleted file mode 100644 index f87ac76..0000000 --- a/python-garmin-connect/Badge.go +++ /dev/null @@ -1,59 +0,0 @@ -package connect - -import ( - "fmt" -) - -// Badge describes a badge. -type Badge struct { - ID int `json:"badgeId"` - Key string `json:"badgeKey"` - Name string `json:"badgeName"` - CategoryID int `json:"badgeCategoryId"` - DifficultyID int `json:"badgeDifficultyId"` - Points int `json:"badgePoints"` - TypeID []int `json:"badgeTypeIds"` - SeriesID int `json:"badgeSeriesId"` - Start Time `json:"badgeStartDate"` - End Time `json:"badgeEndDate"` - UserProfileID int `json:"userProfileId"` - FullName string `json:"fullName"` - DisplayName string `json:"displayName"` - EarnedDate Time `json:"badgeEarnedDate"` - EarnedNumber int `json:"badgeEarnedNumber"` - Viewed bool `json:"badgeIsViewed"` - Progress float64 `json:"badgeProgressValue"` - Target float64 `json:"badgeTargetValue"` - UnitID int `json:"badgeUnitId"` - BadgeAssocTypeID int `json:"badgeAssocTypeId"` - BadgeAssocDataID string `json:"badgeAssocDataId"` - BadgeAssocDataName string `json:"badgeAssocDataName"` - EarnedByMe bool `json:"earnedByMe"` - RelatedBadges []Badge `json:"relatedBadges"` - Connections []Badge `json:"connections"` -} - -// BadgeDetail will return details about a badge. -func (c *Client) BadgeDetail(badgeID int) (*Badge, error) { - // Alternative URL: - // https://connect.garmin.com/modern/proxy/badge-service/badge/DISPLAYNAME/earned/detail/BADGEID - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/badge-service/badge/detail/v2/%d", - badgeID) - - badge := new(Badge) - - err := c.getJSON(URL, badge) - - // This is interesting. Garmin returns 400 if an unknown badge is - // requested. We have no way of detecting that, so we silently changes - // the error to ErrNotFound. - if err == ErrBadRequest { - return nil, ErrNotFound - } - - if err != nil { - return nil, err - } - - return badge, nil -} diff --git a/python-garmin-connect/BadgeAttributes.go b/python-garmin-connect/BadgeAttributes.go deleted file mode 100644 index 42fc9be..0000000 --- a/python-garmin-connect/BadgeAttributes.go +++ /dev/null @@ -1,52 +0,0 @@ -package connect - -// Everything from https://connect.garmin.com/modern/proxy/badge-service/badge/attributes - -type BadgeType struct { - ID int `json:"badgeTypeId"` - Key string `json:"badgeTypeKey"` -} - -type BadgeCategory struct { - ID int `json:"badgeCategoryId"` - Key string `json:"badgeCategoryKey"` -} - -type BadgeDifficulty struct { - ID int `json:"badgeDifficultyId"` - Key string `json:"badgeDifficultyKey"` - Points int `json:"badgePoints"` -} - -type BadgeUnit struct { - ID int `json:"badgeUnitId"` - Key string `json:"badgeUnitKey"` -} - -type BadgeAssocType struct { - ID int `json:"badgeAssocTypeId"` - Key string `json:"badgeAssocTypeKey"` -} - -type BadgeAttributes struct { - BadgeTypes []BadgeType `json:"badgeTypes"` - BadgeCategories []BadgeCategory `json:"badgeCategories"` - BadgeDifficulties []BadgeDifficulty `json:"badgeDifficulties"` - BadgeUnits []BadgeUnit `json:"badgeUnits"` - BadgeAssocTypes []BadgeAssocType `json:"badgeAssocTypes"` -} - -// BadgeAttributes retrieves a list of badge attributes. At time of writing -// we're not sure how these can be utilized. -func (c *Client) BadgeAttributes() (*BadgeAttributes, error) { - URL := "https://connect.garmin.com/modern/proxy/badge-service/badge/attributes" - - attributes := new(BadgeAttributes) - - err := c.getJSON(URL, &attributes) - if err != nil { - return nil, err - } - - return attributes, nil -} diff --git a/python-garmin-connect/BadgeStatus.go b/python-garmin-connect/BadgeStatus.go deleted file mode 100644 index 0a801fe..0000000 --- a/python-garmin-connect/BadgeStatus.go +++ /dev/null @@ -1,94 +0,0 @@ -package connect - -// BadgeStatus is the badge status for a Connect user. -type BadgeStatus struct { - ProfileID int `json:"userProfileId"` - Fullname string `json:"fullName"` - DisplayName string `json:"displayName"` - ProUser bool `json:"userPro"` - ProfileImageURLLarge string `json:"profileImageUrlLarge"` - ProfileImageURLMedium string `json:"profileImageUrlMedium"` - ProfileImageURLSmall string `json:"profileImageUrlSmall"` - Level int `json:"userLevel"` - LevelUpdateTime Time `json:"levelUpdateDate"` - Point int `json:"userPoint"` - Badges []Badge `json:"badges"` -} - -// BadgeLeaderBoard returns the leaderboard for points for the currently -// authenticated user. -func (c *Client) BadgeLeaderBoard() ([]BadgeStatus, error) { - URL := "https://connect.garmin.com/modern/proxy/badge-service/badge/leaderboard" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - var proxy struct { - LeaderBoad []BadgeStatus `json:"connections"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - return proxy.LeaderBoad, nil -} - -// BadgeCompare will compare the earned badges of the currently authenticated user against displayName. -func (c *Client) BadgeCompare(displayName string) (*BadgeStatus, *BadgeStatus, error) { - URL := "https://connect.garmin.com/modern/proxy/badge-service/badge/compare/" + displayName - - if !c.authenticated() { - return nil, nil, ErrNotAuthenticated - } - - var proxy struct { - User *BadgeStatus `json:"user"` - Connection *BadgeStatus `json:"connection"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, nil, err - } - - return proxy.User, proxy.Connection, nil -} - -// BadgesEarned will return the list of badges earned by the curently -// authenticated user. -func (c *Client) BadgesEarned() ([]Badge, error) { - URL := "https://connect.garmin.com/modern/proxy/badge-service/badge/earned" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - badges := make([]Badge, 0, 200) - err := c.getJSON(URL, &badges) - if err != nil { - return nil, err - } - - return badges, nil -} - -// BadgesAvailable will return the list of badges not yet earned by the curently -// authenticated user. -func (c *Client) BadgesAvailable() ([]Badge, error) { - URL := "https://connect.garmin.com/modern/proxy/badge-service/badge/available" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - badges := make([]Badge, 0, 200) - err := c.getJSON(URL, &badges) - if err != nil { - return nil, err - } - - return badges, nil -} diff --git a/python-garmin-connect/Calendar.go b/python-garmin-connect/Calendar.go deleted file mode 100644 index 09c4a7f..0000000 --- a/python-garmin-connect/Calendar.go +++ /dev/null @@ -1,111 +0,0 @@ -package connect - -import ( - "fmt" -) - -// CalendarYear describes a Garmin Connect calendar year -type CalendarYear struct { - StartDayOfJanuary int `json:"startDayofJanuary"` - LeapYear bool `json:"leapYear"` - YearItems []YearItem `json:"yearItems"` - YearSummaries []YearSummary `json:"yearSummaries"` -} - -// YearItem describes an item on a Garmin Connect calendar year -type YearItem struct { - Date Date `json:"date"` - Display int `json:"display"` -} - -// YearSummary describes a per-activity-type yearly summary on a Garmin Connect calendar year -type YearSummary struct { - ActivityTypeID int `json:"activityTypeId"` - NumberOfActivities int `json:"numberOfActivities"` - TotalDistance int `json:"totalDistance"` - TotalDuration int `json:"totalDuration"` - TotalCalories int `json:"totalCalories"` -} - -// CalendarMonth describes a Garmin Conenct calendar month -type CalendarMonth struct { - StartDayOfMonth int `json:"startDayOfMonth"` - NumOfDaysInMonth int `json:"numOfDaysInMonth"` - NumOfDaysInPrevMonth int `json:"numOfDaysInPrevMonth"` - Month int `json:"month"` - Year int `json:"year"` - CalendarItems []CalendarItem `json:"calendarItems"` -} - -// CalendarWeek describes a Garmin Connect calendar week -type CalendarWeek struct { - StartDate Date `json:"startDate"` - EndDate Date `json:"endDate"` - NumOfDaysInMonth int `json:"numOfDaysInMonth"` - CalendarItems []CalendarItem `json:"calendarItems"` -} - -// CalendarItem describes an activity displayed on a Garmin Connect calendar -type CalendarItem struct { - ID int `json:"id"` - ItemType string `json:"itemType"` - ActivityTypeID int `json:"activityTypeId"` - Title string `json:"title"` - Date Date `json:"date"` - Duration int `json:"duration"` - Distance int `json:"distance"` - Calories int `json:"calories"` - StartTimestampLocal Time `json:"startTimestampLocal"` - ElapsedDuration float64 `json:"elapsedDuration"` - Strokes float64 `json:"strokes"` - MaxSpeed float64 `json:"maxSpeed"` - ShareableEvent bool `json:"shareableEvent"` - AutoCalcCalories bool `json:"autoCalcCalories"` - ProtectedWorkoutSchedule bool `json:"protectedWorkoutSchedule"` - IsParent bool `json:"isParent"` -} - -// CalendarYear will get the activity summaries and list of days active for a given year -func (c *Client) CalendarYear(year int) (*CalendarYear, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/calendar-service/year/%d", - year, - ) - calendarYear := new(CalendarYear) - err := c.getJSON(URL, &calendarYear) - if err != nil { - return nil, err - } - - return calendarYear, nil -} - -// CalendarMonth will get the activities for a given month -func (c *Client) CalendarMonth(year int, month int) (*CalendarMonth, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/calendar-service/year/%d/month/%d", - year, - month-1, // Months in Garmin Connect start from zero - ) - calendarMonth := new(CalendarMonth) - err := c.getJSON(URL, &calendarMonth) - if err != nil { - return nil, err - } - - return calendarMonth, nil -} - -// CalendarWeek will get the activities for a given week. A week will be returned that contains the day requested, not starting with) -func (c *Client) CalendarWeek(year int, month int, week int) (*CalendarWeek, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/calendar-service/year/%d/month/%d/day/%d/start/1", - year, - month-1, // Months in Garmin Connect start from zero - week, - ) - calendarWeek := new(CalendarWeek) - err := c.getJSON(URL, &calendarWeek) - if err != nil { - return nil, err - } - - return calendarWeek, nil -} diff --git a/python-garmin-connect/Client.go b/python-garmin-connect/Client.go deleted file mode 100644 index b3f62a6..0000000 --- a/python-garmin-connect/Client.go +++ /dev/null @@ -1,615 +0,0 @@ -package connect - -import ( - "bufio" - "bytes" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/http/httputil" - "net/url" - "regexp" - "strings" - "time" -) - -const ( - // ErrForbidden will be returned if the client doesn't have access to the - // requested ressource. - ErrForbidden = Error("forbidden") - - // ErrNotFound will be returned if the requested ressource could not be - // found. - ErrNotFound = Error("not found") - - // ErrBadRequest will be returned if Garmin returned a status code 400. - ErrBadRequest = Error("bad request") - - // ErrNoCredentials will be returned if credentials are needed - but none - // are set. - ErrNoCredentials = Error("no credentials set") - - // ErrNotAuthenticated will be returned is the client is not - // authenticated as required by the request. Remember to call - // Authenticate(). - ErrNotAuthenticated = Error("client is not authenticated") - - // ErrWrongCredentials will be returned if the username and/or - // password is not recognized by Garmin Connect. - ErrWrongCredentials = Error("username and/or password not recognized") -) - -const ( - // sessionCookieName is the magic session cookie name. - sessionCookieName = "SESSIONID" - - // cflbCookieName is the cookie used by Cloudflare to pin the request - // to a specific backend. - cflbCookieName = "__cflb" -) - -// Client can be used to access the unofficial Garmin Connect API. -type Client struct { - Email string `json:"email"` - Password string `json:"password"` - SessionID string `json:"sessionID"` - Profile *SocialProfile `json:"socialProfile"` - - // LoadBalancerID is the load balancer ID set by Cloudflare in front of - // Garmin Connect. This must be preserves across requests. A session key - // is only valid with a corresponding loadbalancer key. - LoadBalancerID string `json:"cflb"` - - client *http.Client - autoRenewSession bool - debugLogger Logger - dumpWriter io.Writer -} - -// Option is the type to set options on the client. -type Option func(*Client) - -// SessionID will set a predefined session ID. This can be useful for clients -// keeping state. A few HTTP roundtrips can be saved, if the session ID is -// reused. And some load would be taken of Garmin servers. This must be -// accompanied by LoadBalancerID. -// Generally this should not be used. Users of this package should save -// all exported fields from Client and re-use those at a later request. -// json.Marshal() and json.Unmarshal() can be used. -func SessionID(sessionID string) Option { - return func(c *Client) { - c.SessionID = sessionID - } -} - -// LoadBalancerID will set a load balancer ID. This is used by Garmin load -// balancers to route subsequent requests to the same backend server. -func LoadBalancerID(loadBalancerID string) Option { - return func(c *Client) { - c.LoadBalancerID = loadBalancerID - } -} - -// Credentials can be used to pass login credentials to NewClient. -func Credentials(email string, password string) Option { - return func(c *Client) { - c.Email = email - c.Password = password - } -} - -// AutoRenewSession will set if the session should be autorenewed upon expire. -// Default is true. -func AutoRenewSession(autoRenew bool) Option { - return func(c *Client) { - c.autoRenewSession = autoRenew - } -} - -// DebugLogger is used to set a debug logger. -func DebugLogger(logger Logger) Option { - return func(c *Client) { - c.debugLogger = logger - } -} - -// DumpWriter will instruct Client to dump all HTTP requests and responses to -// and from Garmin to w. -func DumpWriter(w io.Writer) Option { - return func(c *Client) { - c.dumpWriter = w - } -} - -// NewClient returns a new client for accessing the unofficial Garmin Connect -// API. -func NewClient(options ...Option) *Client { - client := &Client{ - client: &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - // To avoid a Cloudflare error, we have to use TLS 1.1 or 1.2. - MinVersion: tls.VersionTLS11, - MaxVersion: tls.VersionTLS12, - }, - }, - }, - autoRenewSession: true, - debugLogger: &discardLog{}, - dumpWriter: nil, - } - - client.SetOptions(options...) - - return client -} - -// SetOptions can be used to set various options on Client. -func (c *Client) SetOptions(options ...Option) { - for _, option := range options { - option(c) - } -} - -func (c *Client) dump(reqResp interface{}) { - if c.dumpWriter == nil { - return - } - - var dump []byte - switch obj := reqResp.(type) { - case *http.Request: - _, _ = c.dumpWriter.Write([]byte("\n\nREQUEST\n")) - dump, _ = httputil.DumpRequestOut(obj, true) - case *http.Response: - _, _ = c.dumpWriter.Write([]byte("\n\nRESPONSE\n")) - dump, _ = httputil.DumpResponse(obj, true) - default: - panic("unsupported type") - } - - _, _ = c.dumpWriter.Write(dump) -} - -// addCookies adds needed cookies to a http request if the values are known. -func (c *Client) addCookies(req *http.Request) { - if c.SessionID != "" { - req.AddCookie(&http.Cookie{ - Value: c.SessionID, - Name: sessionCookieName, - }) - } - - if c.LoadBalancerID != "" { - req.AddCookie(&http.Cookie{ - Value: c.LoadBalancerID, - Name: cflbCookieName, - }) - } -} - -func (c *Client) newRequest(method string, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequest(method, url, body) - if err != nil { - return nil, err - } - - // Play nice and give Garmin engineers a way to contact us. - req.Header.Set("User-Agent", "github.com/abrander/garmin-connect") - - // Yep. This is needed for requests sent to the API. No idea what it does. - req.Header.Add("nk", "NT") - - c.addCookies(req) - - return req, nil -} - -func (c *Client) getJSON(url string, target interface{}) error { - req, err := c.newRequest("GET", url, nil) - if err != nil { - return err - } - - resp, err := c.do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - decoder := json.NewDecoder(resp.Body) - - return decoder.Decode(target) -} - -// write is suited for writing stuff to the API when you're NOT expected any -// data in return but a HTTP status code. -func (c *Client) write(method string, url string, payload interface{}, expectedStatus int) error { - var body io.Reader - - if payload != nil { - b, err := json.Marshal(payload) - if err != nil { - return err - } - - body = bytes.NewReader(b) - } - - req, err := c.newRequest(method, url, body) - if err != nil { - return err - } - - // If we have a payload it is by definition JSON. - if payload != nil { - req.Header.Add("content-type", "application/json") - } - - resp, err := c.do(req) - if err != nil { - return err - } - resp.Body.Close() - - if expectedStatus > 0 && resp.StatusCode != expectedStatus { - return fmt.Errorf("HTTP %s returned %d (%d expected)", method, resp.StatusCode, expectedStatus) - } - - return nil -} - -// handleForbidden will try to extract an error message from the response. -func (c *Client) handleForbidden(resp *http.Response) error { - defer resp.Body.Close() - - type proxy struct { - Message string `json:"message"` - Error string `json:"error"` - } - - decoder := json.NewDecoder(resp.Body) - - var errorMessage proxy - - err := decoder.Decode(&errorMessage) - if err == nil && errorMessage.Message != "" { - return Error(errorMessage.Message) - } - - return ErrForbidden -} - -func (c *Client) do(req *http.Request) (*http.Response, error) { - c.debugLogger.Printf("Requesting %s at %s", req.Method, req.URL.String()) - - // Save the body in case we need to replay the request. - var save io.ReadCloser - var err error - if req.Body != nil { - save, req.Body, err = drainBody(req.Body) - if err != nil { - return nil, err - } - } - - c.dump(req) - t0 := time.Now() - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - c.dump(resp) - - // This is exciting. If the user does not have permission to access a - // ressource, the API will return an ApplicationException and return a - // 403 status code. - // If the session is invalid, the Garmin API will return the same exception - // and status code (!). - // To distinguish between these two error cases, we look for a new session - // cookie in the response. If a new session cookies is set by Garmin, we - // assume our current session is invalid. - for _, cookie := range resp.Cookies() { - if cookie.Name == sessionCookieName { - resp.Body.Close() - c.debugLogger.Printf("Session invalid, requesting new session") - - // Wups. Our session got invalidated. - c.SetOptions(SessionID("")) - c.SetOptions(LoadBalancerID("")) - - // Re-new session. - err = c.Authenticate() - if err != nil { - return nil, err - } - - c.debugLogger.Printf("Successfully authenticated as %s", c.Email) - - // Replace the drained body - req.Body = save - - // Replace the cookie ned newRequest with the new sessionid and load balancer key. - req.Header.Del("Cookie") - c.addCookies(req) - - c.debugLogger.Printf("Replaying %s request to %s", req.Method, req.URL.String()) - - c.dump(req) - - // Replay the original request only once, if we fail twice - // something is rotten, and we should give up. - t0 = time.Now() - resp, err = c.client.Do(req) - if err != nil { - return nil, err - } - c.dump(resp) - } - } - - c.debugLogger.Printf("Got HTTP status code %d in %s", resp.StatusCode, time.Since(t0).String()) - - switch resp.StatusCode { - case http.StatusBadRequest: - resp.Body.Close() - return nil, ErrBadRequest - case http.StatusForbidden: - return nil, c.handleForbidden(resp) - case http.StatusNotFound: - resp.Body.Close() - return nil, ErrNotFound - } - - return resp, err -} - -// Download will retrieve a file from url using Garmin Connect credentials. -// It's mostly useful when developing new features or debugging existing -// ones. -// Please note that this will pass the Garmin session cookie to the URL -// provided. Only use this for endpoints on garmin.com. -func (c *Client) Download(url string, w io.Writer) error { - req, err := c.newRequest("GET", url, nil) - if err != nil { - return err - } - - resp, err := c.do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - _, err = io.Copy(w, resp.Body) - if err != nil { - return err - } - - return nil -} - -func (c *Client) authenticated() bool { - return c.SessionID != "" -} - -// Authenticate using a Garmin Connect username and password provided by -// the Credentials option function. -func (c *Client) Authenticate() error { - // We cannot use Client.do() in this function, since this function can be - // called from do() upon session renewal. - URL := "https://sso.garmin.com/sso/signin" + - "?service=https%3A%2F%2Fconnect.garmin.com%2Fmodern%2F" + - "&gauthHost=https%3A%2F%2Fconnect.garmin.com%2Fmodern%2F" + - "&generateExtraServiceTicket=true" + - "&generateTwoExtraServiceTickets=true" - - if c.Email == "" || c.Password == "" { - return ErrNoCredentials - } - - c.debugLogger.Printf("Getting CSRF token at %s", URL) - - // Start by getting CSRF token. - req, err := http.NewRequest("GET", URL, nil) - if err != nil { - return err - } - c.dump(req) - resp, err := c.client.Do(req) - if err != nil { - return err - } - c.dump(resp) - - csrfToken, err := extractCSRFToken(resp.Body) - if err != nil { - return err - } - resp.Body.Close() - - c.debugLogger.Printf("Got CSRF token: '%s'", csrfToken) - - c.debugLogger.Printf("Trying credentials at %s", URL) - - formValues := url.Values{ - "username": {c.Email}, - "password": {c.Password}, - "embed": {"false"}, - "_csrf": {csrfToken}, - } - - req, err = c.newRequest("POST", URL, strings.NewReader(formValues.Encode())) - if err != nil { - return nil - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("Referer", URL) - - c.dump(req) - - resp, err = c.client.Do(req) - if err != nil { - return err - } - c.dump(resp) - - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - - return fmt.Errorf("Garmin SSO returned \"%s\"", resp.Status) - } - - body, _ := ioutil.ReadAll(resp.Body) - resp.Body.Close() - - // Extract ticket URL - t := regexp.MustCompile(`https:\\\/\\\/connect.garmin.com\\\/modern\\\/\?ticket=(([a-zA-Z0-9]|-)*)`) - ticketURL := t.FindString(string(body)) - - // undo escaping - ticketURL = strings.Replace(ticketURL, "\\/", "/", -1) - - if ticketURL == "" { - return ErrWrongCredentials - } - - c.debugLogger.Printf("Requesting session at ticket URL %s", ticketURL) - - // Use ticket to request session. - req, _ = c.newRequest("GET", ticketURL, nil) - c.dump(req) - resp, err = c.client.Do(req) - if err != nil { - return err - } - c.dump(resp) - resp.Body.Close() - - // Look for the needed sessionid cookie. - for _, cookie := range resp.Cookies() { - if cookie.Name == cflbCookieName { - c.debugLogger.Printf("Found load balancer cookie with value %s", cookie.Value) - - c.SetOptions(LoadBalancerID(cookie.Value)) - } - - if cookie.Name == sessionCookieName { - c.debugLogger.Printf("Found session cookie with value %s", cookie.Value) - - c.SetOptions(SessionID(cookie.Value)) - } - } - - if c.SessionID == "" { - c.debugLogger.Printf("No sessionid found") - - return ErrWrongCredentials - } - - // The session id will not be valid until we redeem the sessions by - // following the redirect. - location := resp.Header.Get("Location") - c.debugLogger.Printf("Redeeming session id at %s", location) - - req, _ = c.newRequest("GET", location, nil) - c.dump(req) - resp, err = c.client.Do(req) - if err != nil { - return err - } - c.dump(resp) - - c.Profile, err = extractSocialProfile(resp.Body) - if err != nil { - return err - } - - resp.Body.Close() - - return nil -} - -// extractSocialProfile will try to extract the social profile from the HTML. -// This is very fragile. -func extractSocialProfile(body io.Reader) (*SocialProfile, error) { - scanner := bufio.NewScanner(body) - - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, "VIEWER_SOCIAL_PROFILE") { - line = strings.TrimSpace(line) - line = strings.Replace(line, "\\", "", -1) - line = strings.TrimPrefix(line, "window.VIEWER_SOCIAL_PROFILE = ") - line = strings.TrimSuffix(line, ";") - - profile := new(SocialProfile) - - err := json.Unmarshal([]byte(line), profile) - if err != nil { - return nil, err - } - - return profile, nil - } - } - - return nil, errors.New("social profile not found in HTML") -} - -// extractCSRFToken will try to extract the CSRF token from the signin form. -// This is very fragile. Maybe we should replace this madness by a real HTML -// parser some day. -func extractCSRFToken(body io.Reader) (string, error) { - scanner := bufio.NewScanner(body) - - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, "name=\"_csrf\"") { - line = strings.TrimSpace(line) - line = strings.TrimPrefix(line, ``) - - return line, nil - } - } - - return "", errors.New("CSRF token not found") -} - -// Signout will end the session with Garmin. If you use this for regular -// automated tasks, it would be nice to signout each time to avoid filling -// Garmin's session tables with a lot of short-lived sessions. -func (c *Client) Signout() error { - if !c.authenticated() { - return ErrNotAuthenticated - } - - req, err := c.newRequest("GET", "https://connect.garmin.com/modern/auth/logout", nil) - if err != nil { - return err - } - - if c.SessionID == "" { - return nil - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - c.SetOptions(SessionID("")) - c.SetOptions(LoadBalancerID("")) - - return nil -} diff --git a/python-garmin-connect/Connections.go b/python-garmin-connect/Connections.go deleted file mode 100644 index 0253a3c..0000000 --- a/python-garmin-connect/Connections.go +++ /dev/null @@ -1,111 +0,0 @@ -package connect - -import ( - "encoding/json" - "fmt" - "net/url" - "strings" -) - -// Connections will list the connections of displayName. If displayName is -// empty, the current authenticated users connection list wil be returned. -func (c *Client) Connections(displayName string) ([]SocialProfile, error) { - // There also exist an endpoint without /pagination/ but it will return - // 403 for *some* connections. - URL := "https://connect.garmin.com/modern/proxy/userprofile-service/socialProfile/connections/pagination/" + displayName - - if !c.authenticated() && displayName == "" { - return nil, ErrNotAuthenticated - } - - var proxy struct { - Connections []SocialProfile `json:"userConnections"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - return proxy.Connections, nil -} - -// PendingConnections returns a list of pending connections. -func (c *Client) PendingConnections() ([]SocialProfile, error) { - URL := "https://connect.garmin.com/modern/proxy/userprofile-service/connection/pending" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - pending := make([]SocialProfile, 0, 10) - - err := c.getJSON(URL, &pending) - if err != nil { - return nil, err - } - - return pending, nil -} - -// AcceptConnection will accept a pending connection. -func (c *Client) AcceptConnection(connectionRequestID int) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/userprofile-service/connection/accept/%d", connectionRequestID) - payload := struct { - ConnectionRequestID int `json:"connectionRequestId"` - }{ - ConnectionRequestID: connectionRequestID, - } - - return c.write("PUT", URL, payload, 0) -} - -// SearchConnections can search other users of Garmin Connect. -func (c *Client) SearchConnections(keyword string) ([]SocialProfile, error) { - URL := "https://connect.garmin.com/modern/proxy/usersearch-service/search" - - payload := url.Values{ - "start": {"1"}, - "limit": {"20"}, - "keyword": {keyword}, - } - - req, err := c.newRequest("POST", URL, strings.NewReader(payload.Encode())) - if err != nil { - return nil, err - } - - req.Header.Add("content-type", "application/x-www-form-urlencoded; charset=UTF-8") - - resp, err := c.do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var proxy struct { - Profiles []SocialProfile `json:"profileList"` - } - - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&proxy) - if err != nil { - return nil, err - } - - return proxy.Profiles, nil -} - -// RemoveConnection will remove a connection. -func (c *Client) RemoveConnection(connectionRequestID int) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/userprofile-service/connection/end/%d", connectionRequestID) - - return c.write("PUT", URL, nil, 200) -} - -// RequestConnection will request a connection with displayName. -func (c *Client) RequestConnection(displayName string) error { - URL := "https://connect.garmin.com/modern/proxy/userprofile-service/connection/request/" + displayName - - return c.write("PUT", URL, nil, 0) -} diff --git a/python-garmin-connect/DailyStress.go b/python-garmin-connect/DailyStress.go deleted file mode 100644 index 16825da..0000000 --- a/python-garmin-connect/DailyStress.go +++ /dev/null @@ -1,56 +0,0 @@ -package connect - -import ( - "fmt" - "time" -) - -// StressPoint is a measured stress level at a point in time. -type StressPoint struct { - Timestamp time.Time - Value int -} - -// DailyStress is a stress reading for a single day. -type DailyStress struct { - UserProfilePK int `json:"userProfilePK"` - CalendarDate string `json:"calendarDate"` - StartGMT Time `json:"startTimestampGMT"` - EndGMT Time `json:"endTimestampGMT"` - StartLocal Time `json:"startTimestampLocal"` - EndLocal Time `json:"endTimestampLocal"` - Max int `json:"maxStressLevel"` - Average int `json:"avgStressLevel"` - Values []StressPoint -} - -// DailyStress will retrieve stress levels for date. -func (c *Client) DailyStress(date time.Time) (*DailyStress, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailyStress/%s", - formatDate(date)) - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - // We use a proxy object to deserialize the values to proper Go types. - var proxy struct { - DailyStress - StressValuesArray [][2]int64 `json:"stressValuesArray"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - ret := &proxy.DailyStress - ret.Values = make([]StressPoint, len(proxy.StressValuesArray)) - - for i, point := range proxy.StressValuesArray { - ret.Values[i].Timestamp = time.Unix(point[0]/1000, 0) - ret.Values[i].Value = int(point[1]) - } - - return &proxy.DailyStress, nil -} diff --git a/python-garmin-connect/DailySummary.go b/python-garmin-connect/DailySummary.go deleted file mode 100644 index 6d0866c..0000000 --- a/python-garmin-connect/DailySummary.go +++ /dev/null @@ -1,189 +0,0 @@ -package connect - -import ( - "fmt" - "time" -) - -// DateValue is a numeric value recorded on a given date. -type DateValue struct { - Date Date `json:"calendarDate"` - Value float64 `json:"value"` -} - -// DailySummaries provides a daily summary of various statistics for multiple -// days. -type DailySummaries struct { - Start time.Time `json:"statisticsStartDate"` - End time.Time `json:"statisticsEndDate"` - TotalSteps []DateValue `json:"WELLNESS_TOTAL_STEPS"` - ActiveCalories []DateValue `json:"COMMON_ACTIVE_CALORIES"` - FloorsAscended []DateValue `json:"WELLNESS_FLOORS_ASCENDED"` - IntensityMinutes []DateValue `json:"WELLNESS_USER_INTENSITY_MINUTES_GOAL"` - MaxHeartRate []DateValue `json:"WELLNESS_MAX_HEART_RATE"` - MinimumAverageHeartRate []DateValue `json:"WELLNESS_MIN_AVG_HEART_RATE"` - MinimumHeartrate []DateValue `json:"WELLNESS_MIN_HEART_RATE"` - AverageStress []DateValue `json:"WELLNESS_AVERAGE_STRESS"` - RestingHeartRate []DateValue `json:"WELLNESS_RESTING_HEART_RATE"` - MaxStress []DateValue `json:"WELLNESS_MAX_STRESS"` - AbnormalHeartRateAlers []DateValue `json:"WELLNESS_ABNORMALHR_ALERTS_COUNT"` - MaximumAverageHeartRate []DateValue `json:"WELLNESS_MAX_AVG_HEART_RATE"` - StepGoal []DateValue `json:"WELLNESS_TOTAL_STEP_GOAL"` - FlorsAscendedGoal []DateValue `json:"WELLNESS_USER_FLOORS_ASCENDED_GOAL"` - ModerateIntensityMinutes []DateValue `json:"WELLNESS_MODERATE_INTENSITY_MINUTES"` - TotalColaries []DateValue `json:"WELLNESS_TOTAL_CALORIES"` - BodyBatteryCharged []DateValue `json:"WELLNESS_BODYBATTERY_CHARGED"` - FloorsDescended []DateValue `json:"WELLNESS_FLOORS_DESCENDED"` - BMRCalories []DateValue `json:"WELLNESS_BMR_CALORIES"` - FoodCaloriesRemainin []DateValue `json:"FOOD_CALORIES_REMAINING"` - TotalCalories []DateValue `json:"COMMON_TOTAL_CALORIES"` - BodyBatteryDrained []DateValue `json:"WELLNESS_BODYBATTERY_DRAINED"` - AverageSteps []DateValue `json:"WELLNESS_AVERAGE_STEPS"` - VigorousIntensifyMinutes []DateValue `json:"WELLNESS_VIGOROUS_INTENSITY_MINUTES"` - WellnessDistance []DateValue `json:"WELLNESS_TOTAL_DISTANCE"` - Distance []DateValue `json:"COMMON_TOTAL_DISTANCE"` - WellnessActiveCalories []DateValue `json:"WELLNESS_ACTIVE_CALORIES"` -} - -// DailySummary is an extensive summary for a single day. -type DailySummary struct { - ProfileID int64 `json:"userProfileId"` - TotalKilocalories float64 `json:"totalKilocalories"` - ActiveKilocalories float64 `json:"activeKilocalories"` - BMRKilocalories float64 `json:"bmrKilocalories"` - WellnessKilocalories float64 `json:"wellnessKilocalories"` - BurnedKilocalories float64 `json:"burnedKilocalories"` - ConsumedKilocalories float64 `json:"consumedKilocalories"` - RemainingKilocalories float64 `json:"remainingKilocalories"` - TotalSteps int `json:"totalSteps"` - NetCalorieGoal float64 `json:"netCalorieGoal"` - TotalDistanceMeters int `json:"totalDistanceMeters"` - WellnessDistanceMeters int `json:"wellnessDistanceMeters"` - WellnessActiveKilocalories float64 `json:"wellnessActiveKilocalories"` - NetRemainingKilocalories float64 `json:"netRemainingKilocalories"` - UserID int64 `json:"userDailySummaryId"` - Date Date `json:"calendarDate"` - UUID string `json:"uuid"` - StepGoal int `json:"dailyStepGoal"` - StartTimeGMT Time `json:"wellnessStartTimeGmt"` - EndTimeGMT Time `json:"wellnessEndTimeGmt"` - StartLocal Time `json:"wellnessStartTimeLocal"` - EndLocal Time `json:"wellnessEndTimeLocal"` - Duration time.Duration `json:"durationInMilliseconds"` - Description string `json:"wellnessDescription"` - HighlyActive time.Duration `json:"highlyActiveSeconds"` - Active time.Duration `json:"activeSeconds"` - Sedentary time.Duration `json:"sedentarySeconds"` - Sleeping time.Duration `json:"sleepingSeconds"` - IncludesWellnessData bool `json:"includesWellnessData"` - IncludesActivityData bool `json:"includesActivityData"` - IncludesCalorieConsumedData bool `json:"includesCalorieConsumedData"` - PrivacyProtected bool `json:"privacyProtected"` - ModerateIntensity time.Duration `json:"moderateIntensityMinutes"` - VigorousIntensity time.Duration `json:"vigorousIntensityMinutes"` - FloorsAscendedInMeters float64 `json:"floorsAscendedInMeters"` - FloorsDescendedInMeters float64 `json:"floorsDescendedInMeters"` - FloorsAscended float64 `json:"floorsAscended"` - FloorsDescended float64 `json:"floorsDescended"` - IntensityGoal time.Duration `json:"intensityMinutesGoal"` - FloorsAscendedGoal int `json:"userFloorsAscendedGoal"` - MinHeartRate int `json:"minHeartRate"` - MaxHeartRate int `json:"maxHeartRate"` - RestingHeartRate int `json:"restingHeartRate"` - LastSevenDaysAvgRestingHeartRate int `json:"lastSevenDaysAvgRestingHeartRate"` - Source string `json:"source"` - AverageStress int `json:"averageStressLevel"` - MaxStress int `json:"maxStressLevel"` - Stress time.Duration `json:"stressDuration"` - RestStress time.Duration `json:"restStressDuration"` - ActivityStress time.Duration `json:"activityStressDuration"` - UncategorizedStress time.Duration `json:"uncategorizedStressDuration"` - TotalStress time.Duration `json:"totalStressDuration"` - LowStress time.Duration `json:"lowStressDuration"` - MediumStress time.Duration `json:"mediumStressDuration"` - HighStress time.Duration `json:"highStressDuration"` - StressQualifier string `json:"stressQualifier"` - MeasurableAwake time.Duration `json:"measurableAwakeDuration"` - MeasurableAsleep time.Duration `json:"measurableAsleepDuration"` - LastSyncGMT Time `json:"lastSyncTimestampGMT"` - MinAverageHeartRate int `json:"minAvgHeartRate"` - MaxAverageHeartRate int `json:"maxAvgHeartRate"` -} - -// DailySummary will retrieve a detailed daily summary for date. If -// displayName is empty, the currently authenticated user will be used. -func (c *Client) DailySummary(displayName string, date time.Time) (*DailySummary, error) { - if displayName == "" && c.Profile == nil { - return nil, ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/usersummary-service/usersummary/daily/%s?calendarDate=%s", - displayName, - formatDate(date), - ) - - summary := new(DailySummary) - - err := c.getJSON(URL, summary) - if err != nil { - return nil, err - } - - summary.Duration *= time.Millisecond - summary.HighlyActive *= time.Second - summary.Active *= time.Second - summary.Sedentary *= time.Second - summary.Sleeping *= time.Second - summary.ModerateIntensity *= time.Minute - summary.VigorousIntensity *= time.Minute - summary.IntensityGoal *= time.Minute - summary.Stress *= time.Second - summary.RestStress *= time.Second - summary.ActivityStress *= time.Second - summary.UncategorizedStress *= time.Second - summary.TotalStress *= time.Second - summary.LowStress *= time.Second - summary.MediumStress *= time.Second - summary.HighStress *= time.Second - summary.MeasurableAwake *= time.Second - summary.MeasurableAsleep *= time.Second - - return summary, nil -} - -// DailySummaries will retrieve a daily summary for userID. -func (c *Client) DailySummaries(userID string, from time.Time, until time.Time) (*DailySummaries, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/userstats-service/wellness/daily/%s?fromDate=%s&untilDate=%s", - userID, - formatDate(from), - formatDate(until), - ) - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - // We use a proxy object to deserialize the values to proper Go types. - var proxy struct { - Start Date `json:"statisticsStartDate"` - End Date `json:"statisticsEndDate"` - AllMetrics struct { - Summary DailySummaries `json:"metricsMap"` - } `json:"allMetrics"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - ret := &proxy.AllMetrics.Summary - ret.Start = proxy.Start.Time() - ret.End = proxy.End.Time() - - return ret, nil -} diff --git a/python-garmin-connect/Date.go b/python-garmin-connect/Date.go deleted file mode 100644 index e4a1e81..0000000 --- a/python-garmin-connect/Date.go +++ /dev/null @@ -1,87 +0,0 @@ -package connect - -import ( - "encoding/json" - "fmt" - "strconv" - "time" -) - -// Date represents a single day in Garmin Connect. -type Date struct { - Year int - Month time.Month - DayOfMonth int -} - -// Time returns a time.Time for usage in other packages. -func (d Date) Time() time.Time { - return time.Date(d.Year, d.Month, d.DayOfMonth, 0, 0, 0, 0, time.UTC) -} - -// UnmarshalJSON implements json.Unmarshaler. -func (d *Date) UnmarshalJSON(value []byte) error { - if string(value) == "null" { - return nil - } - - // Sometimes dates are transferred as milliseconds since epoch :-/ - i, err := strconv.ParseInt(string(value), 10, 64) - if err == nil { - t := time.Unix(i/1000, 0) - - d.Year, d.Month, d.DayOfMonth = t.Date() - - return nil - } - - var blip string - err = json.Unmarshal(value, &blip) - if err != nil { - return err - } - - _, err = fmt.Sscanf(blip, "%04d-%02d-%02d", &d.Year, &d.Month, &d.DayOfMonth) - if err != nil { - return err - } - - return nil -} - -// MarshalJSON implements json.Marshaler. -func (d Date) MarshalJSON() ([]byte, error) { - // To better support the Garmin API we marshal the empty value as null. - if d.Year == 0 && d.Month == 0 && d.DayOfMonth == 0 { - return []byte("null"), nil - } - - return []byte(fmt.Sprintf("\"%04d-%02d-%02d\"", d.Year, d.Month, d.DayOfMonth)), nil -} - -// ParseDate will parse a date in the format yyyy-mm-dd. -func ParseDate(in string) (Date, error) { - d := Date{} - - _, err := fmt.Sscanf(in, "%04d-%02d-%02d", &d.Year, &d.Month, &d.DayOfMonth) - - return d, err -} - -// String implements Stringer. -func (d Date) String() string { - if d.Year == 0 && d.Month == 0 && d.DayOfMonth == 0 { - return "-" - } - - return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.DayOfMonth) -} - -// Today will return a Date set to today. -func Today() Date { - d := Date{} - - d.Year, d.Month, d.DayOfMonth = time.Now().Date() - - return d -} diff --git a/python-garmin-connect/Error.go b/python-garmin-connect/Error.go deleted file mode 100644 index f40853d..0000000 --- a/python-garmin-connect/Error.go +++ /dev/null @@ -1,10 +0,0 @@ -package connect - -// Error is a type implementing the error interface. We use this to define -// constant errors. -type Error string - -// Error implements error. -func (e Error) Error() string { - return string(e) -} diff --git a/python-garmin-connect/Gear.go b/python-garmin-connect/Gear.go deleted file mode 100644 index 6cc2f6c..0000000 --- a/python-garmin-connect/Gear.go +++ /dev/null @@ -1,131 +0,0 @@ -package connect - -import ( - "fmt" -) - -// Gear describes a Garmin Connect gear entry -type Gear struct { - Uuid string `json:"uuid"` - GearPk int `json:"gearPk"` - UserProfileID int64 `json:"userProfilePk"` - GearMakeName string `json:"gearMakeName"` - GearModelName string `json:"gearModelName"` - GearTypeName string `json:"gearTypeName"` - DisplayName string `json:"displayName"` - CustomMakeModel string `json:"customMakeModel"` - ImageNameLarge string `json:"imageNameLarge"` - ImageNameMedium string `json:"imageNameMedium"` - ImageNameSmall string `json:"imageNameSmall"` - DateBegin Time `json:"dateBegin"` - DateEnd Time `json:"dateEnd"` - MaximumMeters float64 `json:"maximumMeters"` - Notified bool `json:"notified"` - CreateDate Time `json:"createDate"` - UpdateDate Time `json:"updateDate"` -} - -// GearType desribes the types of gear -type GearType struct { - TypeID int `json:"gearTypePk"` - TypeName string `json:"gearTypeName"` - CreateDate Time `json:"createDate"` - UpdateDate Time `json:"updateData"` -} - -// GearStats describes the stats of gear -type GearStats struct { - TotalDistance float64 `json:"totalDistance"` - TotalActivities int `json:"totalActivities"` - Processsing bool `json:"processing"` -} - -// Gear will retrieve the details of the users gear -func (c *Client) Gear(profileID int64) ([]Gear, error) { - if profileID == 0 && c.Profile == nil { - return nil, ErrNotAuthenticated - } - - if profileID == 0 && c.Profile != nil { - profileID = c.Profile.ProfileID - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/gear-service/gear/filterGear?userProfilePk=%d", - profileID, - ) - var gear []Gear - err := c.getJSON(URL, &gear) - if err != nil { - return nil, err - } - - return gear, nil -} - -// GearType will list the gear types -func (c *Client) GearType() ([]GearType, error) { - URL := "https://connect.garmin.com/modern/proxy/gear-service/gear/types" - var gearType []GearType - err := c.getJSON(URL, &gearType) - if err != nil { - return nil, err - } - - return gearType, nil -} - -// GearStats will get the statistics of an item of gear, given the uuid -func (c *Client) GearStats(uuid string) (*GearStats, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/userstats-service/gears/%s", - uuid, - ) - gearStats := new(GearStats) - err := c.getJSON(URL, &gearStats) - if err != nil { - return nil, err - } - - return gearStats, nil -} - -// GearLink will link an item of gear to an activity. Multiple items of gear can be linked. -func (c *Client) GearLink(uuid string, activityID int) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/gear-service/gear/link/%s/activity/%d", - uuid, - activityID, - ) - - return c.write("PUT", URL, "", 200) -} - -// GearUnlink will remove an item of gear from an activity. All items of gear can be unlinked. -func (c *Client) GearUnlink(uuid string, activityID int) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/gear-service/gear/unlink/%s/activity/%d", - uuid, - activityID, - ) - - return c.write("PUT", URL, "", 200) -} - -// GearForActivity will retrieve the gear associated with an activity -func (c *Client) GearForActivity(profileID int64, activityID int) ([]Gear, error) { - if profileID == 0 && c.Profile == nil { - return nil, ErrNotAuthenticated - } - - if profileID == 0 && c.Profile != nil { - profileID = c.Profile.ProfileID - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/gear-service/gear/filterGear?userProfilePk=%d&activityId=%d", - profileID, activityID, - ) - var gear []Gear - err := c.getJSON(URL, &gear) - if err != nil { - return nil, err - } - - return gear, nil -} diff --git a/python-garmin-connect/Goal.go b/python-garmin-connect/Goal.go deleted file mode 100644 index 76d7961..0000000 --- a/python-garmin-connect/Goal.go +++ /dev/null @@ -1,115 +0,0 @@ -package connect - -import ( - "fmt" -) - -// Goal represents a fitness or health goal. -type Goal struct { - ID int64 `json:"id"` - ProfileID int64 `json:"userProfilePK"` - GoalCategory int `json:"userGoalCategoryPK"` - GoalType GoalType `json:"userGoalTypePK"` - Start Date `json:"startDate"` - End Date `json:"endDate,omitempty"` - Value int `json:"goalValue"` - Created Date `json:"createDate"` -} - -// GoalType represents different types of goals. -type GoalType int - -// String implements Stringer. -func (t GoalType) String() string { - switch t { - case 0: - return "steps-per-day" - case 4: - return "weight" - case 7: - return "floors-ascended" - default: - return fmt.Sprintf("unknown:%d", t) - } -} - -// Goals lists all goals for displayName of type goalType. If displayName is -// empty, the currently authenticated user will be used. -func (c *Client) Goals(displayName string, goalType int) ([]Goal, error) { - if displayName == "" && c.Profile == nil { - return nil, ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/wellness-service/wellness/wellness-goals/%s?userGoalType=%d", - displayName, - goalType, - ) - - goals := make([]Goal, 0, 20) - - err := c.getJSON(URL, &goals) - if err != nil { - return nil, err - } - - return goals, nil -} - -// AddGoal will add a new goal. If displayName is empty, the currently -// authenticated user will be used. -func (c *Client) AddGoal(displayName string, goal Goal) error { - if displayName == "" && c.Profile == nil { - return ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/wellness-service/wellness/wellness-goals/%s", - displayName, - ) - - return c.write("POST", URL, goal, 204) -} - -// DeleteGoal will delete an existing goal. If displayName is empty, the -// currently authenticated user will be used. -func (c *Client) DeleteGoal(displayName string, goalID int) error { - if displayName == "" && c.Profile == nil { - return ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/wellness-service/wellness/wellness-goals/%d/%s", - goalID, - displayName, - ) - - return c.write("DELETE", URL, nil, 204) -} - -// UpdateGoal will update an existing goal. -func (c *Client) UpdateGoal(displayName string, goal Goal) error { - if displayName == "" && c.Profile == nil { - return ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/wellness-service/wellness/wellness-goals/%d/%s", - goal.ID, - displayName, - ) - - return c.write("PUT", URL, goal, 204) -} diff --git a/python-garmin-connect/Group.go b/python-garmin-connect/Group.go deleted file mode 100644 index 0dcd899..0000000 --- a/python-garmin-connect/Group.go +++ /dev/null @@ -1,153 +0,0 @@ -package connect - -import ( - "encoding/json" - "fmt" - "net/url" - "strings" -) - -// Group describes a Garmin Connect group. -type Group struct { - ID int `json:"id"` - Name string `json:"groupName"` - Description string `json:"groupDescription"` - OwnerID int `json:"ownerId"` - ProfileImageURLLarge string `json:"profileImageUrlLarge"` - ProfileImageURLMedium string `json:"profileImageUrlMedium"` - ProfileImageURLSmall string `json:"profileImageUrlSmall"` - Visibility string `json:"groupVisibility"` - Privacy string `json:"groupPrivacy"` - Location string `json:"location"` - WebsiteURL string `json:"websiteUrl"` - FacebookURL string `json:"facebookUrl"` - TwitterURL string `json:"twitterUrl"` - PrimaryActivities []string `json:"primaryActivities"` - OtherPrimaryActivity string `json:"otherPrimaryActivity"` - LeaderboardTypes []string `json:"leaderboardTypes"` - FeatureTypes []string `json:"featureTypes"` - CorporateWellness bool `json:"isCorporateWellness"` - ActivityFeedTypes []ActivityType `json:"activityFeedTypes"` -} - -/* -Unknowns: -"membershipStatus": null, -"isCorporateWellness": false, -"programName": null, -"programTextColor": null, -"programBackgroundColor": null, -"groupMemberCount": null, -*/ - -// Groups will return the group membership. If displayName is empty, the -// currently authenticated user will be used. -func (c *Client) Groups(displayName string) ([]Group, error) { - if displayName == "" && c.Profile == nil { - return nil, ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/group-service/groups/%s", displayName) - - groups := make([]Group, 0, 30) - - err := c.getJSON(URL, &groups) - if err != nil { - return nil, err - } - - return groups, nil -} - -// SearchGroups can search for groups in Garmin Connect. -func (c *Client) SearchGroups(keyword string) ([]Group, error) { - URL := "https://connect.garmin.com/modern/proxy/group-service/keyword" - - payload := url.Values{ - "start": {"1"}, - "limit": {"100"}, - "keyword": {keyword}, - } - - req, err := c.newRequest("POST", URL, strings.NewReader(payload.Encode())) - if err != nil { - return nil, err - } - - req.Header.Add("content-type", "application/x-www-form-urlencoded; charset=UTF-8") - - resp, err := c.do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var proxy struct { - Groups []Group `json:"groupDTOs"` - } - - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&proxy) - if err != nil { - return nil, err - } - - return proxy.Groups, nil -} - -// Group returns details about groupID. -func (c *Client) Group(groupID int) (*Group, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/group-service/group/%d", groupID) - - group := new(Group) - - err := c.getJSON(URL, group) - if err != nil { - return nil, err - } - - return group, nil -} - -// JoinGroup joins a group. If profileID is 0, the currently authenticated -// user will be used. -func (c *Client) JoinGroup(groupID int) error { - if c.Profile == nil { - return ErrNotAuthenticated - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/group-service/group/%d/member/%d", - groupID, - c.Profile.ProfileID, - ) - - payload := struct { - GroupID int `json:"groupId"` - Role *string `json:"groupRole"` // is always null? - ProfileID int64 `json:"userProfileId"` - }{ - groupID, - nil, - c.Profile.ProfileID, - } - - return c.write("POST", URL, payload, 200) -} - -// LeaveGroup leaves a group. -func (c *Client) LeaveGroup(groupID int) error { - if c.Profile == nil { - return ErrNotAuthenticated - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/group-service/group/%d/member/%d", - groupID, - c.Profile.ProfileID, - ) - - return c.write("DELETE", URL, nil, 204) -} diff --git a/python-garmin-connect/GroupAnnouncement.go b/python-garmin-connect/GroupAnnouncement.go deleted file mode 100644 index d15590c..0000000 --- a/python-garmin-connect/GroupAnnouncement.go +++ /dev/null @@ -1,31 +0,0 @@ -package connect - -import ( - "fmt" -) - -// GroupAnnouncement describes a group announcement. Only one announcement can -// exist per group. -type GroupAnnouncement struct { - ID int `json:"announcementId"` - GroupID int `json:"groupId"` - Title string `json:"title"` - Message string `json:"message"` - ExpireDate Time `json:"expireDate"` - AnnouncementDate Time `json:"announcementDate"` -} - -// GroupAnnouncement returns the announcement for groupID. -func (c *Client) GroupAnnouncement(groupID int) (*GroupAnnouncement, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/group-service/group/%d/announcement", - groupID, - ) - - announcement := new(GroupAnnouncement) - err := c.getJSON(URL, announcement) - if err != nil { - return nil, err - } - - return announcement, nil -} diff --git a/python-garmin-connect/GroupMember.go b/python-garmin-connect/GroupMember.go deleted file mode 100644 index d9a56e8..0000000 --- a/python-garmin-connect/GroupMember.go +++ /dev/null @@ -1,60 +0,0 @@ -package connect - -import ( - "fmt" - "time" -) - -// GroupMember describes a member of a group. -type GroupMember struct { - SocialProfile - - Joined time.Time `json:"joinDate"` - Role string `json:"groupRole"` -} - -// GroupMembers will return the member list of a group. -func (c *Client) GroupMembers(groupID int) ([]GroupMember, error) { - type proxy struct { - ID string `json:"id"` - GroupID int `json:"groupId"` - UserProfileID int64 `json:"userProfileId"` - DisplayName string `json:"displayName"` - Location string `json:"location"` - Joined Date `json:"joinDate"` - Role string `json:"groupRole"` - Name string `json:"fullName"` - ProfileImageURLLarge string `json:"profileImageLarge"` - ProfileImageURLMedium string `json:"profileImageMedium"` - ProfileImageURLSmall string `json:"profileImageSmall"` - Pro bool `json:"userPro"` - Level int `json:"userLevel"` - } - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/group-service/group/%d/members", - groupID, - ) - - membersProxy := make([]proxy, 0, 100) - err := c.getJSON(URL, &membersProxy) - if err != nil { - return nil, err - } - - members := make([]GroupMember, len(membersProxy)) - for i, p := range membersProxy { - members[i].DisplayName = p.DisplayName - members[i].ProfileID = p.UserProfileID - members[i].DisplayName = p.DisplayName - members[i].Location = p.Location - members[i].Fullname = p.Name - members[i].ProfileImageURLLarge = p.ProfileImageURLLarge - members[i].ProfileImageURLMedium = p.ProfileImageURLMedium - members[i].ProfileImageURLSmall = p.ProfileImageURLSmall - members[i].UserLevel = p.Level - - members[i].Joined = p.Joined.Time() - members[i].Role = p.Role - } - - return members, nil -} diff --git a/python-garmin-connect/LICENSE b/python-garmin-connect/LICENSE deleted file mode 100644 index a07c1c7..0000000 --- a/python-garmin-connect/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Anders Brander - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/python-garmin-connect/LastUsed.go b/python-garmin-connect/LastUsed.go deleted file mode 100644 index 81a5bb8..0000000 --- a/python-garmin-connect/LastUsed.go +++ /dev/null @@ -1,27 +0,0 @@ -package connect - -// LastUsed describes the last synchronization. -type LastUsed struct { - DeviceID int `json:"userDeviceId"` - ProfileNumber int `json:"userProfileNumber"` - ApplicationNumber int `json:"applicationNumber"` - DeviceApplicationKey string `json:"lastUsedDeviceApplicationKey"` - DeviceName string `json:"lastUsedDeviceName"` - DeviceUploadTime Time `json:"lastUsedDeviceUploadTime"` - ImageURL string `json:"imageUrl"` - Released bool `json:"released"` -} - -// LastUsed will return information about the latest synchronization. -func (c *Client) LastUsed(displayName string) (*LastUsed, error) { - URL := "https://connect.garmin.com/modern/proxy/device-service/deviceservice/userlastused/" + displayName - - lastused := new(LastUsed) - - err := c.getJSON(URL, lastused) - if err != nil { - return nil, err - } - - return lastused, err -} diff --git a/python-garmin-connect/LifetimeActivities.go b/python-garmin-connect/LifetimeActivities.go deleted file mode 100644 index 933753e..0000000 --- a/python-garmin-connect/LifetimeActivities.go +++ /dev/null @@ -1,34 +0,0 @@ -package connect - -import ( - "errors" -) - -// LifetimeActivities is describing a basic summary of all activities. -type LifetimeActivities struct { - Activities int `json:"totalActivities"` // The number of activities - Distance float64 `json:"totalDistance"` // The total distance in meters - Duration float64 `json:"totalDuration"` // The duration of all activities in seconds - Calories float64 `json:"totalCalories"` // Energy in C - ElevationGain float64 `json:"totalElevationGain"` // Total elevation gain in meters -} - -// LifetimeActivities will return some aggregated data about all activities. -func (c *Client) LifetimeActivities(displayName string) (*LifetimeActivities, error) { - URL := "https://connect.garmin.com/modern/proxy/userstats-service/statistics/" + displayName - - var proxy struct { - Activities []LifetimeActivities `json:"userMetrics"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, err - } - - if len(proxy.Activities) != 1 { - return nil, errors.New("unexpected data") - } - - return &proxy.Activities[0], err -} diff --git a/python-garmin-connect/LifetimeTotals.go b/python-garmin-connect/LifetimeTotals.go deleted file mode 100644 index e8dba23..0000000 --- a/python-garmin-connect/LifetimeTotals.go +++ /dev/null @@ -1,25 +0,0 @@ -package connect - -// LifetimeTotals is ligetime statistics for the Connect user. -type LifetimeTotals struct { - ProfileID int `json:"userProfileId"` - ActiveDays int `json:"totalActiveDays"` - Calories float64 `json:"totalCalories"` - Distance int `json:"totalDistance"` - GoalsMetInDays int `json:"totalGoalsMetInDays"` - Steps int `json:"totalSteps"` -} - -// LifetimeTotals returns some lifetime statistics for displayName. -func (c *Client) LifetimeTotals(displayName string) (*LifetimeTotals, error) { - URL := "https://connect.garmin.com/modern/proxy/usersummary-service/stats/connectLifetimeTotals/" + displayName - - totals := new(LifetimeTotals) - - err := c.getJSON(URL, totals) - if err != nil { - return nil, err - } - - return totals, err -} diff --git a/python-garmin-connect/Logger.go b/python-garmin-connect/Logger.go deleted file mode 100644 index 7e8dde4..0000000 --- a/python-garmin-connect/Logger.go +++ /dev/null @@ -1,11 +0,0 @@ -package connect - -// Logger defines the interface understood by the Connect client for logging. -type Logger interface { - Printf(format string, v ...interface{}) -} - -type discardLog struct{} - -func (*discardLog) Printf(format string, v ...interface{}) { -} diff --git a/python-garmin-connect/PersonalInformation.go b/python-garmin-connect/PersonalInformation.go deleted file mode 100644 index d7e6d89..0000000 --- a/python-garmin-connect/PersonalInformation.go +++ /dev/null @@ -1,39 +0,0 @@ -package connect - -// BiometricProfile holds key biometric data. -type BiometricProfile struct { - UserID int `json:"userId"` - Height float64 `json:"height"` - Weight float64 `json:"weight"` // grams - VO2Max float64 `json:"vo2Max"` - VO2MaxCycling float64 `json:"vo2MaxCycling"` -} - -// UserInfo is very basic information about a user. -type UserInfo struct { - Gender string `json:"genderType"` - Email string `json:"email"` - Locale string `json:"locale"` - TimeZone string `json:"timezone"` - Age int `json:"age"` -} - -// PersonalInformation is user info and a biometric profile for a user. -type PersonalInformation struct { - UserInfo UserInfo `json:"userInfo"` - BiometricProfile BiometricProfile `json:"biometricProfile"` -} - -// PersonalInformation will retrieve personal information for displayName. -func (c *Client) PersonalInformation(displayName string) (*PersonalInformation, error) { - URL := "https://connect.garmin.com/modern/proxy/userprofile-service/userprofile/personal-information/" + displayName - - pi := new(PersonalInformation) - - err := c.getJSON(URL, pi) - if err != nil { - return nil, err - } - - return pi, nil -} diff --git a/python-garmin-connect/README.md b/python-garmin-connect/README.md deleted file mode 100644 index b9a14c4..0000000 --- a/python-garmin-connect/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# garmin-connect - -Golang client for the Garmin Connect API. - -This is nothing but a proof of concept, and the API may change at any time. - -[![GoDoc][1]][2] - -[1]: https://godoc.org/github.com/abrander/garmin-connect?status.svg -[2]: https://godoc.org/github.com/abrander/garmin-connect - -# Install - -The `connect` CLI app can be installed using `go install`, and the package using `go get`. - -``` -go install github.com/abrander/garmin-connect/connect@latest -``` - -``` -go get github.com/abrander/garmin-connect@latest -``` diff --git a/python-garmin-connect/SleepState.go b/python-garmin-connect/SleepState.go deleted file mode 100644 index 9b4bb64..0000000 --- a/python-garmin-connect/SleepState.go +++ /dev/null @@ -1,52 +0,0 @@ -package connect - -// SleepState is used to describe the state of sleep with a device capable -// of measuring sleep health. -type SleepState int - -// Known sleep states in Garmin Connect. -const ( - SleepStateUnknown SleepState = -1 - SleepStateDeep SleepState = 0 - SleepStateLight SleepState = 1 - SleepStateREM SleepState = 2 - SleepStateAwake SleepState = 3 -) - -// UnmarshalJSON implements json.Unmarshaler. -func (s *SleepState) UnmarshalJSON(value []byte) error { - // Garmin abuses floats to transfers enums. We ignore the value, and - // simply compares them as strings. - switch string(value) { - case "0.0": - *s = SleepStateDeep - case "1.0": - *s = SleepStateLight - case "2.0": - *s = SleepStateREM - case "3.0": - *s = SleepStateAwake - default: - *s = SleepStateUnknown - } - - return nil -} - -// Sleep implements fmt.Stringer. -func (s SleepState) String() string { - m := map[SleepState]string{ - SleepStateUnknown: "Unknown", - SleepStateDeep: "Deep", - SleepStateLight: "Light", - SleepStateREM: "REM", - SleepStateAwake: "Awake", - } - - str, found := m[s] - if !found { - str = m[SleepStateUnknown] - } - - return str -} diff --git a/python-garmin-connect/SleepSummary.go b/python-garmin-connect/SleepSummary.go deleted file mode 100644 index 49d0317..0000000 --- a/python-garmin-connect/SleepSummary.go +++ /dev/null @@ -1,89 +0,0 @@ -package connect - -import ( - "fmt" - "time" -) - -// "sleepQualityTypePK": null, -// "sleepResultTypePK": null, - -// SleepSummary is a summary of sleep for a single night. -type SleepSummary struct { - ID int64 `json:"id"` - UserProfilePK int64 `json:"userProfilePK"` - Sleep time.Duration `json:"sleepTimeSeconds"` - Nap time.Duration `json:"napTimeSeconds"` - Confirmed bool `json:"sleepWindowConfirmed"` - Confirmation string `json:"sleepWindowConfirmationType"` - StartGMT Time `json:"sleepStartTimestampGMT"` - EndGMT Time `json:"sleepEndTimestampGMT"` - StartLocal Time `json:"sleepStartTimestampLocal"` - EndLocal Time `json:"sleepEndTimestampLocal"` - AutoStartGMT Time `json:"autoSleepStartTimestampGMT"` - AutoEndGMT Time `json:"autoSleepEndTimestampGMT"` - Unmeasurable time.Duration `json:"unmeasurableSleepSeconds"` - Deep time.Duration `json:"deepSleepSeconds"` - Light time.Duration `json:"lightSleepSeconds"` - REM time.Duration `json:"remSleepSeconds"` - Awake time.Duration `json:"awakeSleepSeconds"` - DeviceRemCapable bool `json:"deviceRemCapable"` - REMData bool `json:"remData"` -} - -// SleepMovement denotes the amount of movement for a short time period -// during sleep. -type SleepMovement struct { - Start Time `json:"startGMT"` - End Time `json:"endGMT"` - Level float64 `json:"activityLevel"` -} - -// SleepLevel represents the sleep level for a longer period of time. -type SleepLevel struct { - Start Time `json:"startGMT"` - End Time `json:"endGMT"` - State SleepState `json:"activityLevel"` -} - -// SleepData will retrieve sleep data for date for a given displayName. If -// displayName is empty, the currently authenticated user will be used. -func (c *Client) SleepData(displayName string, date time.Time) (*SleepSummary, []SleepMovement, []SleepLevel, error) { - if displayName == "" && c.Profile == nil { - return nil, nil, nil, ErrNotAuthenticated - } - - if displayName == "" && c.Profile != nil { - displayName = c.Profile.DisplayName - } - - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailySleepData/%s?date=%s&nonSleepBufferMinutes=60", - displayName, - formatDate(date), - ) - - var proxy struct { - SleepSummary SleepSummary `json:"dailySleepDTO"` - REMData bool `json:"remSleepData"` - Movement []SleepMovement `json:"sleepMovement"` - Levels []SleepLevel `json:"sleepLevels"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, nil, nil, err - } - - // All timings from Garmin are in seconds. - proxy.SleepSummary.Sleep *= time.Second - proxy.SleepSummary.Nap *= time.Second - proxy.SleepSummary.Unmeasurable *= time.Second - proxy.SleepSummary.Deep *= time.Second - proxy.SleepSummary.Light *= time.Second - proxy.SleepSummary.REM *= time.Second - proxy.SleepSummary.Awake *= time.Second - - proxy.SleepSummary.REMData = proxy.REMData - - return &proxy.SleepSummary, proxy.Movement, proxy.Levels, nil -} diff --git a/python-garmin-connect/SocialProfile.go b/python-garmin-connect/SocialProfile.go deleted file mode 100644 index 35afed6..0000000 --- a/python-garmin-connect/SocialProfile.go +++ /dev/null @@ -1,79 +0,0 @@ -package connect - -// SocialProfile represents a Garmin Connect user. -type SocialProfile struct { - ID int64 `json:"id"` - ProfileID int64 `json:"profileId"` - ConnectionRequestID int `json:"connectionRequestId"` - GarminGUID string `json:"garminGUID"` - DisplayName string `json:"displayName"` - Fullname string `json:"fullName"` - Username string `json:"userName"` - ProfileImageURLLarge string `json:"profileImageUrlLarge"` - ProfileImageURLMedium string `json:"profileImageUrlMedium"` - ProfileImageURLSmall string `json:"profileImageUrlSmall"` - Location string `json:"location"` - FavoriteActivityTypes []string `json:"favoriteActivityTypes"` - UserRoles []string `json:"userRoles"` - UserProfileFullName string `json:"userProfileFullName"` - UserLevel int `json:"userLevel"` - UserPoint int `json:"userPoint"` -} - -// SocialProfile retrieves a profile for a Garmin Connect user. If displayName -// is empty, the profile for the currently authenticated user will be returned. -func (c *Client) SocialProfile(displayName string) (*SocialProfile, error) { - URL := "https://connect.garmin.com/modern/proxy/userprofile-service/socialProfile/" + displayName - - profile := new(SocialProfile) - - err := c.getJSON(URL, profile) - if err != nil { - return nil, err - } - - return profile, err -} - -// PublicSocialProfile retrieves the public profile for displayName. -func (c *Client) PublicSocialProfile(displayName string) (*SocialProfile, error) { - URL := "https://connect.garmin.com/modern/proxy/userprofile-service/socialProfile/public/" + displayName - - profile := new(SocialProfile) - - err := c.getJSON(URL, profile) - if err != nil { - return nil, err - } - - return profile, err -} - -// BlockedUsers returns the list of blocked users for the currently -// authenticated user. -func (c *Client) BlockedUsers() ([]SocialProfile, error) { - URL := "https://connect.garmin.com/modern/proxy/userblock-service/blockuser" - - var results []SocialProfile - - err := c.getJSON(URL, &results) - if err != nil { - return nil, err - } - - return results, nil -} - -// BlockUser will block a user. -func (c *Client) BlockUser(displayName string) error { - URL := "https://connect.garmin.com/modern/proxy/userblock-service/blockuser/" + displayName - - return c.write("POST", URL, nil, 200) -} - -// UnblockUser removed displayName from the block list. -func (c *Client) UnblockUser(displayName string) error { - URL := "https://connect.garmin.com/modern/proxy/userblock-service/blockuser/" + displayName - - return c.write("DELETE", URL, nil, 204) -} diff --git a/python-garmin-connect/Time.go b/python-garmin-connect/Time.go deleted file mode 100644 index 709d7a4..0000000 --- a/python-garmin-connect/Time.go +++ /dev/null @@ -1,55 +0,0 @@ -package connect - -import ( - "encoding/json" - "strconv" - "time" -) - -// Time is a type masking a time.Time capable of parsing the JSON from -// Garmin Connect. -type Time struct{ time.Time } - -// UnmarshalJSON implements json.Unmarshaler. It can parse timestamps -// returned from connect.garmin.com. -func (t *Time) UnmarshalJSON(value []byte) error { - // Sometimes timestamps are transferred as milliseconds since epoch :-/ - i, err := strconv.ParseInt(string(value), 10, 64) - if err == nil && i > 1000000000000 { - t.Time = time.Unix(i/1000, 0) - - return nil - } - - // FIXME: Somehow we should deal with timezones :-/ - layouts := []string{ - "2006-01-02T15:04:05Z", // Support Gos own format. - "2006-01-02T15:04:05.0", - "2006-01-02 15:04:05", - } - - var blip string - err = json.Unmarshal(value, &blip) - if err != nil { - return err - } - - var proxy time.Time - for _, l := range layouts { - proxy, err = time.Parse(l, blip) - if err == nil { - break - } - } - - t.Time = proxy - - return nil -} - -// MarshalJSON implements json.Marshaler. -func (t *Time) MarshalJSON() ([]byte, error) { - b, err := t.Time.MarshalJSON() - - return b, err -} diff --git a/python-garmin-connect/Time_test.go b/python-garmin-connect/Time_test.go deleted file mode 100644 index 73954df..0000000 --- a/python-garmin-connect/Time_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package connect - -import ( - "encoding/json" - "testing" -) - -func TestTimeUnmarshalJSON(t *testing.T) { - var t0 Time - - input := []byte(`"2019-01-12T11:45:23.0"`) - - err := json.Unmarshal(input, &t0) - if err != nil { - t.Fatalf("Error parsing %s: %s", string(input), err.Error()) - } - - if t0.String() != "2019-01-12 11:45:23 +0000 UTC" { - t.Errorf("Failed to parse `%s` correct, got %s", string(input), t0.String()) - } -} diff --git a/python-garmin-connect/Timezone.go b/python-garmin-connect/Timezone.go deleted file mode 100644 index c51871a..0000000 --- a/python-garmin-connect/Timezone.go +++ /dev/null @@ -1,20 +0,0 @@ -package connect - -import ( - "time" -) - -// Timezone represents a timezone in Garmin Connect. -type Timezone struct { - ID int `json:"unitId"` - Key string `json:"unitKey"` - GMTOffset float64 `json:"gmtOffset"` - DSTOffset float64 `json:"dstOffset"` - Group int `json:"groupNumber"` - TimeZone string `json:"timeZone"` -} - -// Location will (try to) return a location for use with time.Time functions. -func (t *Timezone) Location() (*time.Location, error) { - return time.LoadLocation(t.Key) -} diff --git a/python-garmin-connect/Timezones.go b/python-garmin-connect/Timezones.go deleted file mode 100644 index 1fbde8c..0000000 --- a/python-garmin-connect/Timezones.go +++ /dev/null @@ -1,44 +0,0 @@ -package connect - -// Timezones is the list of known time zones in Garmin Connect. -type Timezones []Timezone - -// Timezones will retrieve the list of known timezones in Garmin Connect. -func (c *Client) Timezones() (Timezones, error) { - URL := "https://connect.garmin.com/modern/proxy/system-service/timezoneUnits" - - if !c.authenticated() { - return nil, ErrNotAuthenticated - } - - timezones := make(Timezones, 0, 100) - - err := c.getJSON(URL, &timezones) - if err != nil { - return nil, err - } - - return timezones, nil -} - -// FindID will search for the timezone with id. -func (ts Timezones) FindID(id int) (Timezone, bool) { - for _, t := range ts { - if t.ID == id { - return t, true - } - } - - return Timezone{}, false -} - -// FindKey will search for the timezone with key key. -func (ts Timezones) FindKey(key string) (Timezone, bool) { - for _, t := range ts { - if t.Key == key { - return t, true - } - } - - return Timezone{}, false -} diff --git a/python-garmin-connect/Weight.go b/python-garmin-connect/Weight.go deleted file mode 100644 index 9391e80..0000000 --- a/python-garmin-connect/Weight.go +++ /dev/null @@ -1,167 +0,0 @@ -package connect - -import ( - "fmt" - "time" -) - -// Weightin is a single weight event. -type Weightin struct { - Date Date `json:"date"` - Version int `json:"version"` - Weight float64 `json:"weight"` // gram - BMI float64 `json:"bmi"` // weight / height² - BodyFatPercentage float64 `json:"bodyFat"` // percent - BodyWater float64 `json:"bodyWater"` // kilogram - BoneMass int `json:"boneMass"` // gram - MuscleMass int `json:"muscleMass"` // gram - SourceType string `json:"sourceType"` -} - -// WeightAverage is aggregated weight data for a specific period. -type WeightAverage struct { - Weightin - From int `json:"from"` - Until int `json:"until"` -} - -// LatestWeight will retrieve the latest weight by date. -func (c *Client) LatestWeight(date time.Time) (*Weightin, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/weight-service/weight/latest?date=%04d-%02d-%02d", - date.Year(), - date.Month(), - date.Day()) - - wi := new(Weightin) - - err := c.getJSON(URL, wi) - if err != nil { - return nil, err - } - - return wi, nil -} - -// Weightins will retrieve all weight ins between startDate and endDate. A -// summary is provided as well. This summary is calculated by Garmin Connect. -func (c *Client) Weightins(startDate time.Time, endDate time.Time) (*WeightAverage, []Weightin, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/weight-service/weight/dateRange?startDate=%s&endDate=%s", - formatDate(startDate), - formatDate(endDate)) - - // An alternative endpoint for weight info this can be found here: - // https://connect.garmin.com/modern/proxy/userprofile-service/userprofile/personal-information/weightWithOutbound?from=1556359100000&until=1556611800000 - - if !c.authenticated() { - return nil, nil, ErrNotAuthenticated - } - - var proxy struct { - DateWeightList []Weightin `json:"dateWeightList"` - TotalAverage *WeightAverage `json:"totalAverage"` - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return nil, nil, err - } - - return proxy.TotalAverage, proxy.DateWeightList, nil -} - -// DeleteWeightin will delete all biometric data for date. -func (c *Client) DeleteWeightin(date time.Time) error { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/biometric-service/biometric/%s", formatDate(date)) - - if !c.authenticated() { - return ErrNotAuthenticated - } - - return c.write("DELETE", URL, nil, 204) -} - -// AddUserWeight will add a manual weight in. weight is in grams to match -// Weightin. -func (c *Client) AddUserWeight(date time.Time, weight float64) error { - URL := "https://connect.garmin.com/modern/proxy/weight-service/user-weight" - payload := struct { - Date string `json:"date"` - UnitKey string `json:"unitKey"` - Value float64 `json:"value"` - }{ - Date: formatDate(date), - UnitKey: "kg", - Value: weight / 1000.0, - } - - return c.write("POST", URL, payload, 204) -} - -// WeightByDate retrieves the weight of date if available. If no weight data -// for date exists, it will return ErrNotFound. -func (c *Client) WeightByDate(date time.Time) (Time, float64, error) { - URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/biometric-service/biometric/weightByDate?date=%s", - formatDate(date)) - - if !c.authenticated() { - return Time{}, 0.0, ErrNotAuthenticated - } - - var proxy []struct { - TimeStamp Time `json:"weightDate"` - Weight float64 `json:"weight"` // gram - } - - err := c.getJSON(URL, &proxy) - if err != nil { - return Time{}, 0.0, err - } - - if len(proxy) < 1 { - return Time{}, 0.0, ErrNotFound - } - - return proxy[0].TimeStamp, proxy[0].Weight, nil -} - -// WeightGoal will list the users weight goal if any. If displayName is empty, -// the currently authenticated user will be used. -func (c *Client) WeightGoal(displayName string) (*Goal, error) { - goals, err := c.Goals(displayName, 4) - if err != nil { - return nil, err - } - - if len(goals) < 1 { - return nil, ErrNotFound - } - - return &goals[0], nil -} - -// SetWeightGoal will set a new weight goal. -func (c *Client) SetWeightGoal(goal int) error { - if !c.authenticated() || c.Profile == nil { - return ErrNotAuthenticated - } - - g := Goal{ - Created: Today(), - Start: Today(), - GoalType: 4, - ProfileID: c.Profile.ProfileID, - Value: goal, - } - - goals, err := c.Goals("", 4) - if err != nil { - return err - } - - if len(goals) >= 1 { - g.ID = goals[0].ID - return c.UpdateGoal("", g) - } - - return c.AddGoal(c.Profile.DisplayName, g) -} diff --git a/python-garmin-connect/connect/.gitignore b/python-garmin-connect/connect/.gitignore deleted file mode 100644 index 6b9fe89..0000000 --- a/python-garmin-connect/connect/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/connect diff --git a/python-garmin-connect/connect/README.md b/python-garmin-connect/connect/README.md deleted file mode 100644 index 4b3e0f5..0000000 --- a/python-garmin-connect/connect/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a simple CLI client for Garmin Connect. diff --git a/python-garmin-connect/connect/Table.go b/python-garmin-connect/connect/Table.go deleted file mode 100644 index 112ae7f..0000000 --- a/python-garmin-connect/connect/Table.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "fmt" - "io" - "unicode/utf8" -) - -type Table struct { - columnsMax []int - header []string - rows [][]string -} - -func NewTable() *Table { - return &Table{} -} - -func (t *Table) AddHeader(titles ...string) { - t.header = titles - t.columnsMax = make([]int, len(t.header)) - for i, title := range t.header { - t.columnsMax[i] = utf8.RuneCountInString(title) - } -} - -func (t *Table) AddRow(columns ...interface{}) { - cols := sliceStringer(columns) - - if len(columns) != len(t.header) { - panic("worng number of columns") - } - - t.rows = append(t.rows, cols) - - for i, col := range cols { - l := utf8.RuneCountInString(col) - - if t.columnsMax[i] < l { - t.columnsMax[i] = l - } - } -} - -func rightPad(in string, length int) string { - result := in - inLen := utf8.RuneCountInString(in) - - for i := 0; i < length-inLen; i++ { - result += " " - } - - return result -} - -func (t *Table) outputLine(w io.Writer, columns []string) { - line := "" - - for i, column := range columns { - line += rightPad(column, t.columnsMax[i]) + " " - } - - fmt.Fprintf(w, "%s\n", line) -} - -func (t *Table) outputHeader(w io.Writer, columns []string) { - line := "" - - for i, column := range columns { - line += "\033[1m" + rightPad(column, t.columnsMax[i]) + "\033[0m " - } - - fmt.Fprintf(w, "%s\n", line) -} - -func (t *Table) Output(writer io.Writer) { - t.outputHeader(writer, t.header) - for _, row := range t.rows { - t.outputLine(writer, row) - } -} diff --git a/python-garmin-connect/connect/Tabular.go b/python-garmin-connect/connect/Tabular.go deleted file mode 100644 index e8c4213..0000000 --- a/python-garmin-connect/connect/Tabular.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "fmt" - "io" - "unicode/utf8" -) - -type Tabular struct { - maxLength int - titles []string - values []Value -} - -type Value struct { - Unit string - Value interface{} -} - -func (v Value) String() string { - str := stringer(v.Value) - - return "\033[1m" + str + "\033[0m " + v.Unit -} - -func NewTabular() *Tabular { - return &Tabular{} -} - -func (t *Tabular) AddValue(title string, value interface{}) { - t.AddValueUnit(title, value, "") -} - -func (t *Tabular) AddValueUnit(title string, value interface{}, unit string) { - v := Value{ - Unit: unit, - Value: value, - } - - t.titles = append(t.titles, title) - t.values = append(t.values, v) - - if len(title) > t.maxLength { - t.maxLength = len(title) - } -} - -func leftPad(in string, length int) string { - result := "" - inLen := utf8.RuneCountInString(in) - - for i := 0; i < length-inLen; i++ { - result += " " - } - - return result + in -} - -func (t *Tabular) Output(writer io.Writer) { - for i, value := range t.values { - fmt.Fprintf(writer, "%s %s\n", leftPad(t.titles[i], t.maxLength), value.String()) - } -} diff --git a/python-garmin-connect/connect/activities.go b/python-garmin-connect/connect/activities.go deleted file mode 100644 index 0bdf22a..0000000 --- a/python-garmin-connect/connect/activities.go +++ /dev/null @@ -1,217 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - - "github.com/spf13/cobra" - - connect "github.com/abrander/garmin-connect" -) - -var ( - exportFormat string - offset int - count int -) - -func init() { - activitiesCmd := &cobra.Command{ - Use: "activities", - } - rootCmd.AddCommand(activitiesCmd) - - activitiesListCmd := &cobra.Command{ - Use: "list [display name]", - Short: "List Activities", - Run: activitiesList, - Args: cobra.RangeArgs(0, 1), - } - activitiesListCmd.Flags().IntVarP(&offset, "offset", "o", 0, "Paginating index where the list starts from") - activitiesListCmd.Flags().IntVarP(&count, "count", "c", 100, "Count of elements to return") - activitiesCmd.AddCommand(activitiesListCmd) - - activitiesViewCmd := &cobra.Command{ - Use: "view ", - Short: "View details for an activity", - Run: activitiesView, - Args: cobra.ExactArgs(1), - } - activitiesCmd.AddCommand(activitiesViewCmd) - - activitiesViewWeatherCmd := &cobra.Command{ - Use: "weather ", - Short: "View weather for an activity", - Run: activitiesViewWeather, - Args: cobra.ExactArgs(1), - } - activitiesViewCmd.AddCommand(activitiesViewWeatherCmd) - - activitiesViewHRZonesCmd := &cobra.Command{ - Use: "hrzones ", - Short: "View hr zones for an activity", - Run: activitiesViewHRZones, - Args: cobra.ExactArgs(1), - } - activitiesViewCmd.AddCommand(activitiesViewHRZonesCmd) - - activitiesExportCmd := &cobra.Command{ - Use: "export ", - Short: "Export an activity to a file", - Run: activitiesExport, - Args: cobra.ExactArgs(1), - } - activitiesExportCmd.Flags().StringVarP(&exportFormat, "format", "f", "fit", "Format of export (fit, tcx, gpx, kml, csv)") - activitiesCmd.AddCommand(activitiesExportCmd) - - activitiesImportCmd := &cobra.Command{ - Use: "import ", - Short: "Import an activity from a file", - Run: activitiesImport, - Args: cobra.ExactArgs(1), - } - activitiesCmd.AddCommand(activitiesImportCmd) - - activitiesDeleteCmd := &cobra.Command{ - Use: "delete ", - Short: "Delete an activity", - Run: activitiesDelete, - Args: cobra.ExactArgs(1), - } - activitiesCmd.AddCommand(activitiesDeleteCmd) - - activitiesRenameCmd := &cobra.Command{ - Use: "rename ", - Short: "Rename an activity", - Run: activitiesRename, - Args: cobra.ExactArgs(2), - } - activitiesCmd.AddCommand(activitiesRenameCmd) -} - -func activitiesList(_ *cobra.Command, args []string) { - displayName := "" - - if len(args) == 1 { - displayName = args[0] - } - - activities, err := client.Activities(displayName, offset, count) - bail(err) - - t := NewTable() - t.AddHeader("ID", "Date", "Name", "Type", "Distance", "Time", "Avg/Max HR", "Calories") - for _, a := range activities { - t.AddRow( - a.ID, - a.StartLocal.Time, - a.ActivityName, - a.ActivityType.TypeKey, - a.Distance, - a.StartLocal, - fmt.Sprintf("%.0f/%.0f", a.AverageHeartRate, a.MaxHeartRate), - a.Calories, - ) - } - t.Output(os.Stdout) -} - -func activitiesView(_ *cobra.Command, args []string) { - activityID, err := strconv.Atoi(args[0]) - bail(err) - - activity, err := client.Activity(activityID) - bail(err) - - t := NewTabular() - t.AddValue("ID", activity.ID) - t.AddValue("Name", activity.ActivityName) - t.Output(os.Stdout) -} - -func activitiesViewWeather(_ *cobra.Command, args []string) { - activityID, err := strconv.Atoi(args[0]) - bail(err) - - weather, err := client.ActivityWeather(activityID) - bail(err) - - t := NewTabular() - t.AddValueUnit("Temperature", weather.Temperature, "°F") - t.AddValueUnit("Apparent Temperature", weather.ApparentTemperature, "°F") - t.AddValueUnit("Dew Point", weather.DewPoint, "°F") - t.AddValueUnit("Relative Humidity", weather.RelativeHumidity, "%") - t.AddValueUnit("Wind Direction", weather.WindDirection, weather.WindDirectionCompassPoint) - t.AddValueUnit("Wind Speed", weather.WindSpeed, "mph") - t.AddValue("Latitude", weather.Latitude) - t.AddValue("Longitude", weather.Longitude) - t.Output(os.Stdout) -} - -func activitiesViewHRZones(_ *cobra.Command, args []string) { - activityID, err := strconv.Atoi(args[0]) - bail(err) - - zones, err := client.ActivityHrZones(activityID) - bail(err) - - t := NewTabular() - //for (zone in zones) - for i := 0; i < len(zones)-1; i++ { - t.AddValue(fmt.Sprintf("Zone %d (%3d-%3dbpm)", zones[i].ZoneNumber, zones[i].ZoneLowBoundary, zones[i+1].ZoneLowBoundary), - zones[i].TimeInZone) - } - t.AddValue(fmt.Sprintf("Zone %d ( > %dbpm )", zones[len(zones)-1].ZoneNumber, zones[len(zones)-1].ZoneLowBoundary), - zones[len(zones)-1].TimeInZone) - - t.Output(os.Stdout) -} - -func activitiesExport(_ *cobra.Command, args []string) { - format, err := connect.FormatFromExtension(exportFormat) - bail(err) - - activityID, err := strconv.Atoi(args[0]) - bail(err) - - name := fmt.Sprintf("%d.%s", activityID, format.Extension()) - f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - bail(err) - - err = client.ExportActivity(activityID, f, format) - bail(err) -} - -func activitiesImport(_ *cobra.Command, args []string) { - filename := args[0] - - f, err := os.Open(filename) - bail(err) - - format, err := connect.FormatFromFilename(filename) - bail(err) - - id, err := client.ImportActivity(f, format) - bail(err) - - fmt.Printf("Activity ID %d imported\n", id) -} - -func activitiesDelete(_ *cobra.Command, args []string) { - activityID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.DeleteActivity(activityID) - bail(err) -} - -func activitiesRename(_ *cobra.Command, args []string) { - activityID, err := strconv.Atoi(args[0]) - bail(err) - - newName := args[1] - - err = client.RenameActivity(activityID, newName) - bail(err) -} diff --git a/python-garmin-connect/connect/badges.go b/python-garmin-connect/connect/badges.go deleted file mode 100644 index 2406879..0000000 --- a/python-garmin-connect/connect/badges.go +++ /dev/null @@ -1,222 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - - "github.com/spf13/cobra" - - connect "github.com/abrander/garmin-connect" -) - -const gotIt = "✓" - -func init() { - badgesCmd := &cobra.Command{ - Use: "badges", - } - rootCmd.AddCommand(badgesCmd) - - badgesLeaderboardCmd := &cobra.Command{ - Use: "leaderboard", - Short: "Show the current points leaderbaord among the authenticated users connections", - Run: badgesLeaderboard, - Args: cobra.NoArgs, - } - badgesCmd.AddCommand(badgesLeaderboardCmd) - - badgesEarnedCmd := &cobra.Command{ - Use: "earned [display name]", - Short: "Show the earned badges", - Run: badgesEarned, - Args: cobra.RangeArgs(0, 1), - } - badgesCmd.AddCommand(badgesEarnedCmd) - - badgesAvailableCmd := &cobra.Command{ - Use: "available", - Short: "Show badges not yet earned", - Run: badgesAvailable, - Args: cobra.NoArgs, - } - badgesCmd.AddCommand(badgesAvailableCmd) - - badgesViewCmd := &cobra.Command{ - Use: "view ", - Short: "Show details about a badge", - Run: badgesView, - Args: cobra.ExactArgs(1), - } - badgesCmd.AddCommand(badgesViewCmd) - - badgesCompareCmd := &cobra.Command{ - Use: "compare ", - Short: "Compare the authenticated users badges with the badges of another user", - Run: badgesCompare, - Args: cobra.ExactArgs(1), - } - badgesCmd.AddCommand(badgesCompareCmd) -} - -func badgesLeaderboard(_ *cobra.Command, _ []string) { - leaderboard, err := client.BadgeLeaderBoard() - bail(err) - - t := NewTable() - t.AddHeader("Display Name", "Name", "Level", "Points") - for _, status := range leaderboard { - t.AddRow(status.DisplayName, status.Fullname, status.Level, status.Point) - } - t.Output(os.Stdout) -} - -func badgesEarned(_ *cobra.Command, args []string) { - var badges []connect.Badge - - if len(args) == 1 { - displayName := args[0] - // If we have a displayid to show, we abuse the compare call to read - // badges earned by a connection. - _, status, err := client.BadgeCompare(displayName) - bail(err) - - badges = status.Badges - } else { - var err error - badges, err = client.BadgesEarned() - bail(err) - } - - t := NewTable() - t.AddHeader("ID", "Badge", "Points", "Date") - for _, badge := range badges { - p := fmt.Sprintf("%d", badge.Points) - if badge.EarnedNumber > 1 { - p = fmt.Sprintf("%d x%d", badge.Points, badge.EarnedNumber) - } - t.AddRow(badge.ID, badge.Name, p, badge.EarnedDate.String()) - } - t.Output(os.Stdout) -} - -func badgesAvailable(_ *cobra.Command, _ []string) { - badges, err := client.BadgesAvailable() - bail(err) - - t := NewTable() - t.AddHeader("ID", "Key", "Name", "Points") - for _, badge := range badges { - t.AddRow(badge.ID, badge.Key, badge.Name, badge.Points) - } - t.Output(os.Stdout) -} - -func badgesView(_ *cobra.Command, args []string) { - badgeID, err := strconv.Atoi(args[0]) - bail(err) - - badge, err := client.BadgeDetail(badgeID) - bail(err) - - t := NewTabular() - t.AddValue("ID", badge.ID) - t.AddValue("Key", badge.Key) - t.AddValue("Name", badge.Name) - t.AddValue("Points", badge.Points) - t.AddValue("Earned", formatDate(badge.EarnedDate.Time)) - t.AddValueUnit("Earned", badge.EarnedNumber, "time(s)") - t.AddValue("Available from", formatDate(badge.Start.Time)) - t.AddValue("Available to", formatDate(badge.End.Time)) - t.Output(os.Stdout) - - if len(badge.Connections) > 0 { - fmt.Printf("\n Connections with badge:\n") - t := NewTable() - t.AddHeader("Display Name", "Name", "Earned") - for _, b := range badge.Connections { - t.AddRow(b.DisplayName, b.FullName, b.EarnedDate.Time) - } - t.Output(os.Stdout) - } - - if len(badge.RelatedBadges) > 0 { - fmt.Printf("\n Relates badges:\n") - - t := NewTable() - t.AddHeader("ID", "Key", "Name", "Points", "Earned") - for _, b := range badge.RelatedBadges { - earned := "" - if b.EarnedByMe { - earned = gotIt - } - t.AddRow(b.ID, b.Key, b.Name, b.Points, earned) - } - t.Output(os.Stdout) - } -} - -func badgesCompare(_ *cobra.Command, args []string) { - displayName := args[0] - a, b, err := client.BadgeCompare(displayName) - bail(err) - - t := NewTable() - t.AddHeader("Badge", a.Fullname, b.Fullname, "Points") - - type status struct { - name string - points int - me bool - meEarned int - other bool - otherEarned int - } - - m := map[string]*status{} - - for _, badge := range a.Badges { - s, found := m[badge.Key] - if !found { - s = &status{} - m[badge.Key] = s - } - s.me = true - s.meEarned = badge.EarnedNumber - s.name = badge.Name - s.points = badge.Points - } - - for _, badge := range b.Badges { - s, found := m[badge.Key] - if !found { - s = &status{} - m[badge.Key] = s - } - s.other = true - s.otherEarned = badge.EarnedNumber - s.name = badge.Name - s.points = badge.Points - } - - for _, e := range m { - var me string - var other string - if e.me { - me = gotIt - if e.meEarned > 1 { - me += fmt.Sprintf(" %dx", e.meEarned) - } - } - - if e.other { - other = gotIt - if e.otherEarned > 1 { - other += fmt.Sprintf(" %dx", e.otherEarned) - } - } - t.AddRow(e.name, me, other, e.points) - } - - t.Output(os.Stdout) -} diff --git a/python-garmin-connect/connect/calendar.go b/python-garmin-connect/connect/calendar.go deleted file mode 100644 index 135992a..0000000 --- a/python-garmin-connect/connect/calendar.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "os" - "strconv" - - "github.com/spf13/cobra" -) - -func init() { - calendarCmd := &cobra.Command{ - Use: "calendar", - } - rootCmd.AddCommand(calendarCmd) - - calendarYearCmd := &cobra.Command{ - Use: "year ", - Short: "List active days in the year", - Run: calendarYear, - Args: cobra.RangeArgs(1, 1), - } - calendarCmd.AddCommand(calendarYearCmd) - - calendarMonthCmd := &cobra.Command{ - Use: "month ", - Short: "List active days in the month", - Run: calendarMonth, - Args: cobra.RangeArgs(2, 2), - } - calendarCmd.AddCommand(calendarMonthCmd) - - calendarWeekCmd := &cobra.Command{ - Use: "week ", - Short: "List active days in the week", - Run: calendarWeek, - Args: cobra.RangeArgs(3, 3), - } - calendarCmd.AddCommand(calendarWeekCmd) - -} - -func calendarYear(_ *cobra.Command, args []string) { - year, err := strconv.ParseInt(args[0], 10, 32) - bail(err) - - calendar, err := client.CalendarYear(int(year)) - bail(err) - - t := NewTable() - t.AddHeader("ActivityType ID", "Number of Activities", "Total Distance", "Total Duration", "Total Calories") - for _, summary := range calendar.YearSummaries { - t.AddRow( - summary.ActivityTypeID, - summary.NumberOfActivities, - summary.TotalDistance, - summary.TotalDuration, - summary.TotalCalories, - ) - } - t.Output(os.Stdout) -} - -func calendarMonth(_ *cobra.Command, args []string) { - year, err := strconv.ParseInt(args[0], 10, 32) - bail(err) - - month, err := strconv.ParseInt(args[1], 10, 32) - bail(err) - - calendar, err := client.CalendarMonth(int(year), int(month)) - bail(err) - - t := NewTable() - t.AddHeader("ID", "Date", "Name", "Distance", "Time", "Calories") - for _, item := range calendar.CalendarItems { - t.AddRow( - item.ID, - item.Date, - item.Title, - item.Distance, - item.ElapsedDuration, - item.Calories, - ) - } - t.Output(os.Stdout) -} - -func calendarWeek(_ *cobra.Command, args []string) { - year, err := strconv.ParseInt(args[0], 10, 32) - bail(err) - - month, err := strconv.ParseInt(args[1], 10, 32) - bail(err) - - week, err := strconv.ParseInt(args[2], 10, 32) - bail(err) - - calendar, err := client.CalendarWeek(int(year), int(month), int(week)) - bail(err) - - t := NewTable() - t.AddHeader("ID", "Date", "Name", "Distance", "Time", "Calories") - for _, item := range calendar.CalendarItems { - t.AddRow( - item.ID, - item.Date, - item.Title, - item.Distance, - item.ElapsedDuration, - item.Calories, - ) - } - t.Output(os.Stdout) -} diff --git a/python-garmin-connect/connect/challenges.go b/python-garmin-connect/connect/challenges.go deleted file mode 100644 index dfc696c..0000000 --- a/python-garmin-connect/connect/challenges.go +++ /dev/null @@ -1,169 +0,0 @@ -package main - -import ( - "os" - "strconv" - "strings" - - "github.com/spf13/cobra" -) - -func init() { - challengesCmd := &cobra.Command{ - Use: "challenges", - } - rootCmd.AddCommand(challengesCmd) - - challengesListCmd := &cobra.Command{ - Use: "list", - Short: "List ad-hoc challenges", - Run: challengesList, - Args: cobra.NoArgs, - } - challengesCmd.AddCommand(challengesListCmd) - - challengesListInvitesCmd := &cobra.Command{ - Use: "invites", - Short: "List ad-hoc challenge invites", - Run: challengesListInvites, - Args: cobra.NoArgs, - } - challengesListCmd.AddCommand(challengesListInvitesCmd) - - challengesAcceptCmd := &cobra.Command{ - Use: "accept ", - Short: "Accept an ad-hoc challenge", - Run: challengesAccept, - Args: cobra.ExactArgs(1), - } - challengesCmd.AddCommand(challengesAcceptCmd) - - challengesDeclineCmd := &cobra.Command{ - Use: "decline ", - Short: "Decline an ad-hoc challenge", - Run: challengesDecline, - Args: cobra.ExactArgs(1), - } - challengesCmd.AddCommand(challengesDeclineCmd) - - challengesListPreviousCmd := &cobra.Command{ - Use: "previous", - Short: "Show completed ad-hoc challenges", - Run: challengesListPrevious, - Args: cobra.NoArgs, - } - challengesListCmd.AddCommand(challengesListPreviousCmd) - - challengesViewCmd := &cobra.Command{ - Use: "view ", - Short: "View challenge details", - Run: challengesView, - Args: cobra.ExactArgs(1), - } - challengesCmd.AddCommand(challengesViewCmd) - - challengesLeaveCmd := &cobra.Command{ - Use: "leave ", - Short: "Leave a challenge", - Run: challengesLeave, - Args: cobra.ExactArgs(1), - } - challengesCmd.AddCommand(challengesLeaveCmd) - - challengesRemoveCmd := &cobra.Command{ - Use: "remove ", - Short: "Remove a user from a challenge", - Run: challengesRemove, - Args: cobra.ExactArgs(2), - } - challengesCmd.AddCommand(challengesRemoveCmd) -} - -func challengesList(_ *cobra.Command, args []string) { - challenges, err := client.AdhocChallenges() - bail(err) - - t := NewTable() - t.AddHeader("ID", "Start", "End", "Description", "Name", "Rank") - for _, c := range challenges { - t.AddRow(c.UUID, c.Start, c.End, c.Description, c.Name, c.UserRanking) - } - t.Output(os.Stdout) -} - -func challengesListInvites(_ *cobra.Command, _ []string) { - challenges, err := client.AdhocChallengeInvites() - bail(err) - - t := NewTable() - t.AddHeader("Invite ID", "Challenge ID", "Start", "End", "Description", "Name", "Rank") - for _, c := range challenges { - t.AddRow(c.InviteID, c.UUID, c.Start, c.End, c.Description, c.Name, c.UserRanking) - } - t.Output(os.Stdout) -} - -func challengesAccept(_ *cobra.Command, args []string) { - inviteID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.AdhocChallengeInvitationRespond(inviteID, true) - bail(err) -} - -func challengesDecline(_ *cobra.Command, args []string) { - inviteID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.AdhocChallengeInvitationRespond(inviteID, false) - bail(err) -} - -func challengesListPrevious(_ *cobra.Command, args []string) { - challenges, err := client.HistoricalAdhocChallenges() - bail(err) - - t := NewTable() - t.AddHeader("ID", "Start", "End", "Description", "Name", "Rank") - for _, c := range challenges { - t.AddRow(c.UUID, c.Start, c.End, c.Description, c.Name, c.UserRanking) - } - t.Output(os.Stdout) -} - -func challengesLeave(_ *cobra.Command, args []string) { - uuid := args[0] - err := client.LeaveAdhocChallenge(uuid, 0) - bail(err) -} - -func challengesRemove(_ *cobra.Command, args []string) { - uuid := args[0] - - profileID, err := strconv.ParseInt(args[1], 10, 64) - bail(err) - - err = client.LeaveAdhocChallenge(uuid, profileID) - bail(err) -} - -func challengesView(_ *cobra.Command, args []string) { - uuid := args[0] - challenge, err := client.AdhocChallenge(uuid) - bail(err) - - players := make([]string, len(challenge.Players)) - for i, player := range challenge.Players { - players[i] = player.FullName + " [" + player.DisplayName + "]" - } - - t := NewTabular() - t.AddValue("ID", challenge.UUID) - t.AddValue("Start", challenge.Start.String()) - t.AddValue("End", challenge.End.String()) - t.AddValue("Description", challenge.Description) - t.AddValue("Name", challenge.Name) - t.AddValue("Rank", challenge.UserRanking) - t.AddValue("Players", strings.Join(players, ", ")) - t.Output(os.Stdout) -} diff --git a/python-garmin-connect/connect/completion.go b/python-garmin-connect/connect/completion.go deleted file mode 100644 index f5b6129..0000000 --- a/python-garmin-connect/connect/completion.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "os" - - "github.com/spf13/cobra" -) - -func init() { - completionCmd := &cobra.Command{ - Use: "completion", - } - rootCmd.AddCommand(completionCmd) - - completionBashCmd := &cobra.Command{ - Use: "bash", - Short: "Output command completion for Bourne Again Shell (bash)", - RunE: completionBash, - Args: cobra.NoArgs, - } - completionCmd.AddCommand(completionBashCmd) - - completionZshCmd := &cobra.Command{ - Use: "zsh", - Short: "Output command completion for Z Shell (zsh)", - RunE: completionZsh, - Args: cobra.NoArgs, - } - completionCmd.AddCommand(completionZshCmd) -} - -func completionBash(_ *cobra.Command, _ []string) error { - return rootCmd.GenBashCompletion(os.Stdout) -} - -func completionZsh(_ *cobra.Command, _ []string) error { - return rootCmd.GenZshCompletion(os.Stdout) -} diff --git a/python-garmin-connect/connect/connections.go b/python-garmin-connect/connect/connections.go deleted file mode 100644 index eb0523d..0000000 --- a/python-garmin-connect/connect/connections.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "os" - "strconv" - - "github.com/spf13/cobra" -) - -func init() { - connectionsCmd := &cobra.Command{ - Use: "connections", - } - rootCmd.AddCommand(connectionsCmd) - - connectionsListCmd := &cobra.Command{ - Use: "list [display name]", - Short: "List all connections", - Run: connectionsList, - Args: cobra.RangeArgs(0, 1), - } - connectionsCmd.AddCommand(connectionsListCmd) - - connectionsPendingCmd := &cobra.Command{ - Use: "pending", - Short: "List pending connections", - Run: connectionsPending, - Args: cobra.NoArgs, - } - connectionsCmd.AddCommand(connectionsPendingCmd) - - connectionsRemoveCmd := &cobra.Command{ - Use: "remove ", - Short: "Remove a connection", - Run: connectionsRemove, - Args: cobra.ExactArgs(1), - } - connectionsCmd.AddCommand(connectionsRemoveCmd) - - connectionsSearchCmd := &cobra.Command{ - Use: "search ", - Short: "Search Garmin wide for a person", - Run: connectionsSearch, - Args: cobra.ExactArgs(1), - } - connectionsCmd.AddCommand(connectionsSearchCmd) - - connectionsAcceptCmd := &cobra.Command{ - Use: "accept ", - Short: "Accept a connection request", - Run: connectionsAccept, - Args: cobra.ExactArgs(1), - } - connectionsCmd.AddCommand(connectionsAcceptCmd) - - connectionsRequestCmd := &cobra.Command{ - Use: "request ", - Short: "Request connectio from another user", - Run: connectionsRequest, - Args: cobra.ExactArgs(1), - } - connectionsCmd.AddCommand(connectionsRequestCmd) - - blockedCmd := &cobra.Command{ - Use: "blocked", - } - connectionsCmd.AddCommand(blockedCmd) - - blockedListCmd := &cobra.Command{ - Use: "list", - Short: "List currently blocked users", - Run: connectionsBlockedList, - Args: cobra.NoArgs, - } - blockedCmd.AddCommand(blockedListCmd) - - blockedAddCmd := &cobra.Command{ - Use: "add ", - Short: "Add a user to the blocked list", - Run: connectionsBlockedAdd, - Args: cobra.ExactArgs(1), - } - blockedCmd.AddCommand(blockedAddCmd) - - blockedRemoveCmd := &cobra.Command{ - Use: "remove ", - Short: "Remove a user from the blocked list", - Run: connectionsBlockedRemove, - Args: cobra.ExactArgs(1), - } - blockedCmd.AddCommand(blockedRemoveCmd) -} - -func connectionsList(_ *cobra.Command, args []string) { - displayName := "" - if len(args) == 1 { - displayName = args[0] - } - - connections, err := client.Connections(displayName) - bail(err) - - t := NewTable() - t.AddHeader("Connection ID", "Display Name", "Name", "Location", "Profile Image") - for _, c := range connections { - t.AddRow(c.ConnectionRequestID, c.DisplayName, c.Fullname, c.Location, c.ProfileImageURLMedium) - } - t.Output(os.Stdout) -} - -func connectionsPending(_ *cobra.Command, _ []string) { - connections, err := client.PendingConnections() - bail(err) - - t := NewTable() - t.AddHeader("RequestID", "Display Name", "Name", "Location", "Profile Image") - for _, c := range connections { - t.AddRow(c.ConnectionRequestID, c.DisplayName, c.Fullname, c.Location, c.ProfileImageURLMedium) - } - t.Output(os.Stdout) -} - -func connectionsRemove(_ *cobra.Command, args []string) { - connectionRequestID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.RemoveConnection(connectionRequestID) - bail(err) -} - -func connectionsSearch(_ *cobra.Command, args []string) { - keyword := args[0] - connections, err := client.SearchConnections(keyword) - bail(err) - - t := NewTabular() - for _, c := range connections { - t.AddValue(c.DisplayName, c.Fullname) - } - t.Output(os.Stdout) -} - -func connectionsAccept(_ *cobra.Command, args []string) { - connectionRequestID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.AcceptConnection(connectionRequestID) - bail(err) -} - -func connectionsRequest(_ *cobra.Command, args []string) { - displayName := args[0] - - err := client.RequestConnection(displayName) - bail(err) -} - -func connectionsBlockedList(_ *cobra.Command, _ []string) { - blockedUsers, err := client.BlockedUsers() - bail(err) - - t := NewTable() - t.AddHeader("Display Name", "Name", "Location", "Profile Image") - for _, c := range blockedUsers { - t.AddRow(c.DisplayName, c.Fullname, c.Location, c.ProfileImageURLMedium) - } - t.Output(os.Stdout) -} - -func connectionsBlockedAdd(_ *cobra.Command, args []string) { - displayName := args[0] - err := client.BlockUser(displayName) - bail(err) -} - -func connectionsBlockedRemove(_ *cobra.Command, args []string) { - displayName := args[0] - err := client.UnblockUser(displayName) - bail(err) -} diff --git a/python-garmin-connect/connect/gear.go b/python-garmin-connect/connect/gear.go deleted file mode 100644 index 985b949..0000000 --- a/python-garmin-connect/connect/gear.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "os" - "sort" - "strconv" - - "github.com/spf13/cobra" -) - -func init() { - gearCmd := &cobra.Command{ - Use: "gear", - } - rootCmd.AddCommand(gearCmd) - - gearListCmd := &cobra.Command{ - Use: "list [profile ID]", - Short: "List Gear", - Run: gearList, - Args: cobra.RangeArgs(0, 1), - } - gearCmd.AddCommand(gearListCmd) - - gearTypeListCmd := &cobra.Command{ - Use: "types", - Short: "List Gear Types", - Run: gearTypeList, - } - gearCmd.AddCommand(gearTypeListCmd) - - gearLinkCommand := &cobra.Command{ - Use: "link ", - Short: "Link Gear to Activity", - Run: gearLink, - Args: cobra.ExactArgs(2), - } - gearCmd.AddCommand(gearLinkCommand) - - gearUnlinkCommand := &cobra.Command{ - Use: "unlink ", - Short: "Unlink Gear to Activity", - Run: gearUnlink, - Args: cobra.ExactArgs(2), - } - gearCmd.AddCommand(gearUnlinkCommand) - - gearForActivityCommand := &cobra.Command{ - Use: "activity ", - Short: "Get Gear for Activity", - Run: gearForActivity, - Args: cobra.ExactArgs(1), - } - gearCmd.AddCommand(gearForActivityCommand) -} - -func gearList(_ *cobra.Command, args []string) { - var profileID int64 = 0 - var err error - if len(args) == 1 { - profileID, err = strconv.ParseInt(args[0], 10, 64) - bail(err) - } - gear, err := client.Gear(profileID) - bail(err) - - t := NewTable() - t.AddHeader("UUID", "Type", "Brand & Model", "Nickname", "Created Date", "Total Distance", "Activities") - for _, g := range gear { - - gearStats, err := client.GearStats(g.Uuid) - bail(err) - - t.AddRow( - g.Uuid, - g.GearTypeName, - g.CustomMakeModel, - g.DisplayName, - g.CreateDate.Time, - strconv.FormatFloat(gearStats.TotalDistance, 'f', 2, 64), - gearStats.TotalActivities, - ) - } - t.Output(os.Stdout) -} - -func gearTypeList(_ *cobra.Command, _ []string) { - gearTypes, err := client.GearType() - bail(err) - - t := NewTable() - t.AddHeader("ID", "Name", "Created Date", "Update Date") - sort.Slice(gearTypes, func(i, j int) bool { - return gearTypes[i].TypeID < gearTypes[j].TypeID - }) - - for _, g := range gearTypes { - t.AddRow( - g.TypeID, - g.TypeName, - g.CreateDate, - g.UpdateDate, - ) - } - t.Output(os.Stdout) -} - -func gearLink(_ *cobra.Command, args []string) { - uuid := args[0] - activityID, err := strconv.Atoi(args[1]) - bail(err) - - err = client.GearLink(uuid, activityID) - bail(err) -} - -func gearUnlink(_ *cobra.Command, args []string) { - uuid := args[0] - activityID, err := strconv.Atoi(args[1]) - bail(err) - - err = client.GearUnlink(uuid, activityID) - bail(err) -} - -func gearForActivity(_ *cobra.Command, args []string) { - activityID, err := strconv.Atoi(args[0]) - bail(err) - - gear, err := client.GearForActivity(0, activityID) - bail(err) - - t := NewTable() - t.AddHeader("UUID", "Type", "Brand & Model", "Nickname", "Created Date", "Total Distance", "Activities") - for _, g := range gear { - - gearStats, err := client.GearStats(g.Uuid) - bail(err) - - t.AddRow( - g.Uuid, - g.GearTypeName, - g.CustomMakeModel, - g.DisplayName, - g.CreateDate.Time, - strconv.FormatFloat(gearStats.TotalDistance, 'f', 2, 64), - gearStats.TotalActivities, - ) - } - t.Output(os.Stdout) -} diff --git a/python-garmin-connect/connect/get.go b/python-garmin-connect/connect/get.go deleted file mode 100644 index b9fbb2c..0000000 --- a/python-garmin-connect/connect/get.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "io" - "os" - - "github.com/spf13/cobra" -) - -var ( - formatJSON bool -) - -func init() { - getCmd := &cobra.Command{ - Use: "get ", - Short: "Get data from Garmin Connect, print to stdout", - Run: get, - Args: cobra.ExactArgs(1), - } - getCmd.Flags().BoolVarP(&formatJSON, "json", "j", false, "Format output as indented JSON") - rootCmd.AddCommand(getCmd) -} - -func get(_ *cobra.Command, args []string) { - url := args[0] - - if formatJSON { - raw := bytes.NewBuffer(nil) - buffer := bytes.NewBuffer(nil) - - err := client.Download(url, raw) - bail(err) - - err = json.Indent(buffer, raw.Bytes(), "", " ") - bail(err) - - _, err = io.Copy(os.Stdout, buffer) - bail(err) - } else { - err := client.Download(url, os.Stdout) - bail(err) - } -} diff --git a/python-garmin-connect/connect/goals.go b/python-garmin-connect/connect/goals.go deleted file mode 100644 index 931d71c..0000000 --- a/python-garmin-connect/connect/goals.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "os" - "strconv" - - "github.com/spf13/cobra" -) - -func init() { - goalsCmd := &cobra.Command{ - Use: "goals", - } - rootCmd.AddCommand(goalsCmd) - - goalsListCmd := &cobra.Command{ - Use: "list [display name]", - Short: "List all goals", - Run: goalsList, - Args: cobra.RangeArgs(0, 1), - } - goalsCmd.AddCommand(goalsListCmd) - - goalsDeleteCmd := &cobra.Command{ - Use: "delete ", - Short: "Delete a goal", - Run: goalsDelete, - Args: cobra.ExactArgs(1), - } - goalsCmd.AddCommand(goalsDeleteCmd) -} - -func goalsList(_ *cobra.Command, args []string) { - displayName := "" - if len(args) == 1 { - displayName = args[0] - } - - t := NewTable() - t.AddHeader("ID", "Profile", "Category", "Type", "Start", "End", "Created", "Value") - for typ := 0; typ <= 9; typ++ { - goals, err := client.Goals(displayName, typ) - bail(err) - - for _, g := range goals { - t.AddRow( - g.ID, - g.ProfileID, - g.GoalCategory, - g.GoalType, - g.Start, - g.End, - g.Created, - g.Value, - ) - } - } - t.Output(os.Stdout) -} - -func goalsDelete(_ *cobra.Command, args []string) { - goalID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.DeleteGoal("", goalID) - bail(err) -} diff --git a/python-garmin-connect/connect/groups.go b/python-garmin-connect/connect/groups.go deleted file mode 100644 index 4383ab4..0000000 --- a/python-garmin-connect/connect/groups.go +++ /dev/null @@ -1,189 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - "strings" - - "github.com/spf13/cobra" -) - -func init() { - groupsCmd := &cobra.Command{ - Use: "groups", - } - rootCmd.AddCommand(groupsCmd) - - groupsListCmd := &cobra.Command{ - Use: "list [display name]", - Short: "List all groups", - Run: groupsList, - Args: cobra.RangeArgs(0, 1), - } - groupsCmd.AddCommand(groupsListCmd) - - groupsViewCmd := &cobra.Command{ - Use: "view ", - Short: "View group details", - Run: groupsView, - Args: cobra.ExactArgs(1), - } - groupsCmd.AddCommand(groupsViewCmd) - - groupsViewAnnouncementCmd := &cobra.Command{ - Use: "announcement ", - Short: "View group abbouncement", - Run: groupsViewAnnouncement, - Args: cobra.ExactArgs(1), - } - groupsViewCmd.AddCommand(groupsViewAnnouncementCmd) - - groupsViewMembersCmd := &cobra.Command{ - Use: "members ", - Short: "View group members", - Run: groupsViewMembers, - Args: cobra.ExactArgs(1), - } - groupsViewCmd.AddCommand(groupsViewMembersCmd) - - groupsSearchCmd := &cobra.Command{ - Use: "search ", - Short: "Search for a group", - Run: groupsSearch, - Args: cobra.ExactArgs(1), - } - groupsCmd.AddCommand(groupsSearchCmd) - - groupsJoinCmd := &cobra.Command{ - Use: "join ", - Short: "Join a group", - Run: groupsJoin, - Args: cobra.ExactArgs(1), - } - groupsCmd.AddCommand(groupsJoinCmd) - - groupsLeaveCmd := &cobra.Command{ - Use: "leave ", - Short: "Leave a group", - Run: groupsLeave, - Args: cobra.ExactArgs(1), - } - groupsCmd.AddCommand(groupsLeaveCmd) -} - -func groupsList(_ *cobra.Command, args []string) { - displayName := "" - if len(args) == 1 { - displayName = args[0] - } - - groups, err := client.Groups(displayName) - bail(err) - - t := NewTable() - t.AddHeader("ID", "Name", "Description", "Profile Image") - for _, g := range groups { - t.AddRow(g.ID, g.Name, g.Description, g.ProfileImageURLLarge) - } - t.Output(os.Stdout) -} - -func groupsSearch(_ *cobra.Command, args []string) { - keyword := args[0] - groups, err := client.SearchGroups(keyword) - bail(err) - - lastID := 0 - - t := NewTable() - t.AddHeader("ID", "Name", "Description", "Profile Image") - for _, g := range groups { - if g.ID == lastID { - continue - } - - t.AddRow(g.ID, g.Name, g.Description, g.ProfileImageURLLarge) - - lastID = g.ID - } - t.Output(os.Stdout) -} - -func groupsView(_ *cobra.Command, args []string) { - id, err := strconv.Atoi(args[0]) - bail(err) - - group, err := client.Group(id) - bail(err) - - t := NewTabular() - t.AddValue("ID", group.ID) - t.AddValue("Name", group.Name) - t.AddValue("Description", group.Description) - t.AddValue("OwnerID", group.OwnerID) - t.AddValue("ProfileImageURLLarge", group.ProfileImageURLLarge) - t.AddValue("ProfileImageURLMedium", group.ProfileImageURLMedium) - t.AddValue("ProfileImageURLSmall", group.ProfileImageURLSmall) - t.AddValue("Visibility", group.Visibility) - t.AddValue("Privacy", group.Privacy) - t.AddValue("Location", group.Location) - t.AddValue("WebsiteURL", group.WebsiteURL) - t.AddValue("FacebookURL", group.FacebookURL) - t.AddValue("TwitterURL", group.TwitterURL) - // t.AddValue("PrimaryActivities", group.PrimaryActivities) - t.AddValue("OtherPrimaryActivity", group.OtherPrimaryActivity) - // t.AddValue("LeaderboardTypes", group.LeaderboardTypes) - // t.AddValue("FeatureTypes", group.FeatureTypes) - t.AddValue("CorporateWellness", group.CorporateWellness) - // t.AddValue("ActivityFeedTypes", group.ActivityFeedTypes) - t.Output(os.Stdout) -} - -func groupsViewAnnouncement(_ *cobra.Command, args []string) { - id, err := strconv.Atoi(args[0]) - bail(err) - - announcement, err := client.GroupAnnouncement(id) - bail(err) - - t := NewTabular() - t.AddValue("ID", announcement.ID) - t.AddValue("GroupID", announcement.GroupID) - t.AddValue("Title", announcement.Title) - t.AddValue("ExpireDate", announcement.ExpireDate.String()) - t.AddValue("AnnouncementDate", announcement.AnnouncementDate.String()) - t.Output(os.Stdout) - fmt.Fprintf(os.Stdout, "\n%s\n", strings.TrimSpace(announcement.Message)) -} - -func groupsViewMembers(_ *cobra.Command, args []string) { - id, err := strconv.Atoi(args[0]) - bail(err) - - members, err := client.GroupMembers(id) - bail(err) - - t := NewTable() - t.AddHeader("Display Name", "Joined", "Name", "Location", "Role", "Profile Image") - for _, m := range members { - t.AddRow(m.DisplayName, m.Joined, m.Fullname, m.Location, m.Role, m.ProfileImageURLMedium) - } - t.Output(os.Stdout) -} - -func groupsJoin(_ *cobra.Command, args []string) { - groupID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.JoinGroup(groupID) - bail(err) -} - -func groupsLeave(_ *cobra.Command, args []string) { - groupID, err := strconv.Atoi(args[0]) - bail(err) - - err = client.LeaveGroup(groupID) - bail(err) -} diff --git a/python-garmin-connect/connect/info.go b/python-garmin-connect/connect/info.go deleted file mode 100644 index b0269e8..0000000 --- a/python-garmin-connect/connect/info.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "os" - "time" - - connect "github.com/abrander/garmin-connect" - "github.com/spf13/cobra" -) - -func init() { - infoCmd := &cobra.Command{ - Use: "info [display name]", - Short: "Show various information and statistics about a Connect User", - Run: info, - Args: cobra.RangeArgs(0, 1), - } - rootCmd.AddCommand(infoCmd) -} - -func info(_ *cobra.Command, args []string) { - displayName := "" - if len(args) == 1 { - displayName = args[0] - } - - t := NewTabular() - - socialProfile, err := client.SocialProfile(displayName) - if err == connect.ErrNotFound { - bail(err) - } - - if err == nil { - displayName = socialProfile.DisplayName - } else { - socialProfile, err = client.PublicSocialProfile(displayName) - bail(err) - - displayName = socialProfile.DisplayName - } - - t.AddValue("ID", socialProfile.ID) - t.AddValue("Profile ID", socialProfile.ProfileID) - t.AddValue("Display Name", socialProfile.DisplayName) - t.AddValue("Name", socialProfile.Fullname) - t.AddValue("Level", socialProfile.UserLevel) - t.AddValue("Points", socialProfile.UserPoint) - t.AddValue("Profile Image", socialProfile.ProfileImageURLLarge) - - info, err := client.PersonalInformation(displayName) - if err == nil { - t.AddValue("", "") - t.AddValue("Gender", info.UserInfo.Gender) - t.AddValueUnit("Age", info.UserInfo.Age, "years") - t.AddValueUnit("Height", nzf(info.BiometricProfile.Height), "cm") - t.AddValueUnit("Weight", nzf(info.BiometricProfile.Weight/1000.0), "kg") - t.AddValueUnit("Vo² Max", nzf(info.BiometricProfile.VO2Max), "mL/kg/min") - t.AddValueUnit("Vo² Max (cycling)", nzf(info.BiometricProfile.VO2MaxCycling), "mL/kg/min") - } - - life, err := client.LifetimeActivities(displayName) - if err == nil { - t.AddValue("", "") - t.AddValue("Activities", life.Activities) - t.AddValueUnit("Distance", life.Distance/1000.0, "km") - t.AddValueUnit("Time", (time.Duration(life.Duration) * time.Second).Round(time.Second).String(), "hms") - t.AddValueUnit("Calories", life.Calories/4.184, "Kcal") - t.AddValueUnit("Elev Gain", life.ElevationGain, "m") - } - - totals, err := client.LifetimeTotals(displayName) - if err == nil { - t.AddValue("", "") - t.AddValueUnit("Steps", totals.Steps, "steps") - t.AddValueUnit("Distance", totals.Distance/1000.0, "km") - t.AddValueUnit("Daily Goal Met", totals.GoalsMetInDays, "days") - t.AddValueUnit("Active Days", totals.ActiveDays, "days") - if totals.ActiveDays > 0 { - t.AddValueUnit("Average Steps", totals.Steps/totals.ActiveDays, "steps") - } - t.AddValueUnit("Calories", totals.Calories, "kCal") - } - - lastUsed, err := client.LastUsed(displayName) - if err == nil { - t.AddValue("", "") - t.AddValue("Device ID", lastUsed.DeviceID) - t.AddValue("Device", lastUsed.DeviceName) - t.AddValue("Time", lastUsed.DeviceUploadTime.String()) - t.AddValue("Ago", time.Since(lastUsed.DeviceUploadTime.Time).Round(time.Second).String()) - t.AddValue("Image", lastUsed.ImageURL) - } - - t.Output(os.Stdout) -} diff --git a/python-garmin-connect/connect/main.go b/python-garmin-connect/connect/main.go deleted file mode 100644 index 7f96c9e..0000000 --- a/python-garmin-connect/connect/main.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "syscall" - - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - - connect "github.com/abrander/garmin-connect" -) - -var ( - rootCmd = &cobra.Command{ - Use: os.Args[0] + " [command]", - Short: "CLI Client for Garmin Connect", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - loadState() - if verbose { - logger := log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile) - client.SetOptions(connect.DebugLogger(logger)) - } - - if dumpFile != "" { - w, err := os.OpenFile(dumpFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - bail(err) - client.SetOptions(connect.DumpWriter(w)) - } - }, - PersistentPostRun: func(_ *cobra.Command, _ []string) { - storeState() - }, - } - - verbose bool - dumpFile string -) - -func init() { - rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose debug output") - rootCmd.PersistentFlags().StringVarP(&dumpFile, "dump", "d", "", "File to dump requests and responses to") - - authenticateCmd := &cobra.Command{ - Use: "authenticate [email]", - Short: "Authenticate against the Garmin API", - Run: authenticate, - Args: cobra.RangeArgs(0, 1), - } - rootCmd.AddCommand(authenticateCmd) - - signoutCmd := &cobra.Command{ - Use: "signout", - Short: "Log out of the Garmin API and forget session and password", - Run: signout, - Args: cobra.NoArgs, - } - rootCmd.AddCommand(signoutCmd) -} - -func bail(err error) { - if err != nil { - log.Fatalf("%s", err.Error()) - } -} - -func main() { - bail(rootCmd.Execute()) -} - -func authenticate(_ *cobra.Command, args []string) { - var email string - if len(args) == 1 { - email = args[0] - } else { - fmt.Print("Email: ") - fmt.Scanln(&email) - } - - fmt.Print("Password: ") - - password, err := terminal.ReadPassword(syscall.Stdin) - bail(err) - - client.SetOptions(connect.Credentials(email, string(password))) - err = client.Authenticate() - bail(err) - - fmt.Printf("\nSuccess\n") -} - -func signout(_ *cobra.Command, _ []string) { - _ = client.Signout() - client.Password = "" -} diff --git a/python-garmin-connect/connect/nzf.go b/python-garmin-connect/connect/nzf.go deleted file mode 100644 index 0871944..0000000 --- a/python-garmin-connect/connect/nzf.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" -) - -// nzf is a type that will print "-" instead of 0.0 when used as a stringer. -type nzf float64 - -func (nzf nzf) String() string { - if nzf != 0.0 { - return fmt.Sprintf("%.01f", nzf) - } - - return "-" -} diff --git a/python-garmin-connect/connect/sleep.go b/python-garmin-connect/connect/sleep.go deleted file mode 100644 index 6e8b48f..0000000 --- a/python-garmin-connect/connect/sleep.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "fmt" - "os" - - connect "github.com/abrander/garmin-connect" - "github.com/spf13/cobra" -) - -func init() { - sleepCmd := &cobra.Command{ - Use: "sleep", - } - rootCmd.AddCommand(sleepCmd) - - sleepSummaryCmd := &cobra.Command{ - Use: "summary [displayName]", - Short: "Show sleep summary for date", - Run: sleepSummary, - Args: cobra.RangeArgs(1, 2), - } - sleepCmd.AddCommand(sleepSummaryCmd) -} - -func sleepSummary(_ *cobra.Command, args []string) { - date, err := connect.ParseDate(args[0]) - bail(err) - - displayName := "" - - if len(args) > 1 { - displayName = args[1] - } - - summary, _, levels, err := client.SleepData(displayName, date.Time()) - bail(err) - - t := NewTabular() - t.AddValue("Start", summary.StartGMT) - t.AddValue("End", summary.EndGMT) - t.AddValue("Sleep", hoursAndMinutes(summary.Sleep)) - t.AddValue("Nap", hoursAndMinutes(summary.Nap)) - t.AddValue("Unmeasurable", hoursAndMinutes(summary.Unmeasurable)) - t.AddValue("Deep", hoursAndMinutes(summary.Deep)) - t.AddValue("Light", hoursAndMinutes(summary.Light)) - t.AddValue("REM", hoursAndMinutes(summary.REM)) - t.AddValue("Awake", hoursAndMinutes(summary.Awake)) - t.AddValue("Confirmed", summary.Confirmed) - t.AddValue("Confirmation Type", summary.Confirmation) - t.AddValue("REM Data", summary.REMData) - t.Output(os.Stdout) - - fmt.Fprintf(os.Stdout, "\n") - - t2 := NewTable() - t2.AddHeader("Start", "End", "State", "Duration") - for _, l := range levels { - t2.AddRow(l.Start, l.End, l.State, hoursAndMinutes(l.End.Sub(l.Start.Time))) - } - t2.Output(os.Stdout) -} diff --git a/python-garmin-connect/connect/state.go b/python-garmin-connect/connect/state.go deleted file mode 100644 index a3b60f1..0000000 --- a/python-garmin-connect/connect/state.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "log" - "os" - "path" - - connect "github.com/abrander/garmin-connect" -) - -var ( - client = connect.NewClient( - connect.AutoRenewSession(true), - ) - - stateFile string -) - -func init() { - rootCmd.PersistentFlags().StringVarP(&stateFile, "state", "s", stateFilename(), "State file to use") -} - -func stateFilename() string { - home, err := os.UserHomeDir() - if err != nil { - log.Fatalf("Could not detect home directory: %s", err.Error()) - } - - return path.Join(home, ".garmin-connect.json") -} - -func loadState() { - data, err := ioutil.ReadFile(stateFile) - if err != nil { - log.Printf("Could not open state file: %s", err.Error()) - return - } - - err = json.Unmarshal(data, client) - if err != nil { - log.Fatalf("Could not unmarshal state: %s", err.Error()) - } -} - -func storeState() { - b, err := json.MarshalIndent(client, "", " ") - if err != nil { - log.Fatalf("Could not marshal state: %s", err.Error()) - } - - err = ioutil.WriteFile(stateFile, b, 0600) - if err != nil { - log.Fatalf("Could not write state file: %s", err.Error()) - } -} diff --git a/python-garmin-connect/connect/tools.go b/python-garmin-connect/connect/tools.go deleted file mode 100644 index b318b66..0000000 --- a/python-garmin-connect/connect/tools.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "time" -) - -func formatDate(t time.Time) string { - if t == (time.Time{}) { - return "-" - } - - return fmt.Sprintf("%04d-%02d-%02d", t.Year(), t.Month(), t.Day()) -} - -func stringer(value interface{}) string { - stringer, ok := value.(fmt.Stringer) - if ok { - return stringer.String() - } - - str := "" - switch v := value.(type) { - case string: - str = v - case int, int64: - str = fmt.Sprintf("%d", v) - case float64: - str = strconv.FormatFloat(v, 'f', 1, 64) - case bool: - if v { - str = gotIt - } - default: - panic(fmt.Sprintf("no idea what to do about %T:%v", value, value)) - } - - return str -} - -func sliceStringer(values []interface{}) []string { - ret := make([]string, len(values)) - - for i, value := range values { - ret[i] = stringer(value) - } - - return ret -} - -func hoursAndMinutes(dur time.Duration) string { - if dur == 0 { - return "-" - } - - if dur < 60*time.Minute { - m := dur.Truncate(time.Minute) - - return fmt.Sprintf("%dm", m/time.Minute) - } - - h := dur.Truncate(time.Hour) - m := (dur - h).Truncate(time.Minute) - - h /= time.Hour - m /= time.Minute - - return fmt.Sprintf("%dh%dm", h, m) -} diff --git a/python-garmin-connect/connect/weight.go b/python-garmin-connect/connect/weight.go deleted file mode 100644 index 3245849..0000000 --- a/python-garmin-connect/connect/weight.go +++ /dev/null @@ -1,224 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - "time" - - connect "github.com/abrander/garmin-connect" - "github.com/spf13/cobra" -) - -func init() { - weightCmd := &cobra.Command{ - Use: "weight", - } - rootCmd.AddCommand(weightCmd) - - weightLatestCmd := &cobra.Command{ - Use: "latest", - Short: "Show the latest weight-in", - Run: weightLatest, - Args: cobra.NoArgs, - } - weightCmd.AddCommand(weightLatestCmd) - - weightLatestWeekCmd := &cobra.Command{ - Use: "week", - Short: "Show average weight for the latest week", - Run: weightLatestWeek, - Args: cobra.NoArgs, - } - weightLatestCmd.AddCommand(weightLatestWeekCmd) - - weightAddCmd := &cobra.Command{ - Use: "add ", - Short: "Add a simple weight for a specific date", - Run: weightAdd, - Args: cobra.ExactArgs(2), - } - weightCmd.AddCommand(weightAddCmd) - - weightDeleteCmd := &cobra.Command{ - Use: "delete ", - Short: "Delete a weight-in", - Run: weightDelete, - Args: cobra.ExactArgs(1), - } - weightCmd.AddCommand(weightDeleteCmd) - - weightDateCmd := &cobra.Command{ - Use: "date [yyyy-mm-dd]", - Short: "Show weight for a specific date", - Run: weightDate, - Args: cobra.ExactArgs(1), - } - weightCmd.AddCommand(weightDateCmd) - - weightRangeCmd := &cobra.Command{ - Use: "range [yyyy-mm-dd] [yyyy-mm-dd]", - Short: "Show weight for a date range", - Run: weightRange, - Args: cobra.ExactArgs(2), - } - weightCmd.AddCommand(weightRangeCmd) - - weightGoalCmd := &cobra.Command{ - Use: "goal [displayName]", - Short: "Show weight goal", - Run: weightGoal, - Args: cobra.RangeArgs(0, 1), - } - weightCmd.AddCommand(weightGoalCmd) - - weightGoalSetCmd := &cobra.Command{ - Use: "set [goal in gram]", - Short: "Set weight goal", - Run: weightGoalSet, - Args: cobra.ExactArgs(1), - } - weightGoalCmd.AddCommand(weightGoalSetCmd) -} - -func weightLatest(_ *cobra.Command, _ []string) { - weightin, err := client.LatestWeight(time.Now()) - bail(err) - - t := NewTabular() - t.AddValue("Date", weightin.Date.String()) - t.AddValueUnit("Weight", weightin.Weight/1000.0, "kg") - t.AddValueUnit("BMI", weightin.BMI, "kg/m2") - t.AddValueUnit("Fat", weightin.BodyFatPercentage, "%") - t.AddValueUnit("Fat Mass", (weightin.Weight*weightin.BodyFatPercentage)/100000.0, "kg") - t.AddValueUnit("Water", weightin.BodyWater, "%") - t.AddValueUnit("Bone Mass", float64(weightin.BoneMass)/1000.0, "kg") - t.AddValueUnit("Muscle Mass", float64(weightin.MuscleMass)/1000.0, "kg") - t.Output(os.Stdout) -} - -func weightLatestWeek(_ *cobra.Command, _ []string) { - now := time.Now() - from := time.Now().Add(-24 * 6 * time.Hour) - - average, _, err := client.Weightins(from, now) - bail(err) - - t := NewTabular() - t.AddValue("Average from", formatDate(from)) - t.AddValueUnit("Weight", average.Weight/1000.0, "kg") - t.AddValueUnit("BMI", average.BMI, "kg/m2") - t.AddValueUnit("Fat", average.BodyFatPercentage, "%") - t.AddValueUnit("Fat Mass", (average.Weight*average.BodyFatPercentage)/100000.0, "kg") - t.AddValueUnit("Water", average.BodyWater, "%") - t.AddValueUnit("Bone Mass", float64(average.BoneMass)/1000.0, "kg") - t.AddValueUnit("Muscle Mass", float64(average.MuscleMass)/1000.0, "kg") - t.Output(os.Stdout) -} - -func weightAdd(_ *cobra.Command, args []string) { - date, err := connect.ParseDate(args[0]) - bail(err) - - weight, err := strconv.Atoi(args[1]) - bail(err) - - err = client.AddUserWeight(date.Time(), float64(weight)) - bail(err) -} - -func weightDelete(_ *cobra.Command, args []string) { - date, err := connect.ParseDate(args[0]) - bail(err) - - err = client.DeleteWeightin(date.Time()) - bail(err) -} - -func weightDate(_ *cobra.Command, args []string) { - date, err := connect.ParseDate(args[0]) - bail(err) - - tim, weight, err := client.WeightByDate(date.Time()) - bail(err) - - zero := time.Time{} - if tim.Time == zero { - fmt.Printf("No weight ins on this date\n") - os.Exit(1) - } - - t := NewTabular() - t.AddValue("Time", tim.String()) - t.AddValueUnit("Weight", weight/1000.0, "kg") - t.Output(os.Stdout) -} - -func weightRange(_ *cobra.Command, args []string) { - from, err := connect.ParseDate(args[0]) - bail(err) - - to, err := connect.ParseDate(args[1]) - bail(err) - - average, weightins, err := client.Weightins(from.Time(), to.Time()) - bail(err) - - t := NewTabular() - - t.AddValueUnit("Weight", average.Weight/1000.0, "kg") - t.AddValueUnit("BMI", average.BMI, "kg/m2") - t.AddValueUnit("Fat", average.BodyFatPercentage, "%") - t.AddValueUnit("Fat Mass", average.Weight*average.BodyFatPercentage/100000.0, "kg") - t.AddValueUnit("Water", average.BodyWater, "%") - t.AddValueUnit("Bone Mass", float64(average.BoneMass)/1000.0, "kg") - t.AddValueUnit("Muscle Mass", float64(average.MuscleMass)/1000.0, "kg") - fmt.Fprintf(os.Stdout, " \033[1mAverage\033[0m\n") - t.Output(os.Stdout) - - t2 := NewTable() - t2.AddHeader("Date", "Weight", "BMI", "Fat%", "Fat", "Water%", "Bone Mass", "Muscle Mass") - for _, weightin := range weightins { - if weightin.Weight < 1.0 { - continue - } - - t2.AddRow( - weightin.Date, - weightin.Weight/1000.0, - nzf(weightin.BMI), - nzf(weightin.BodyFatPercentage), - nzf(weightin.Weight*weightin.BodyFatPercentage/100000.0), - nzf(weightin.BodyWater), - nzf(float64(weightin.BoneMass)/1000.0), - nzf(float64(weightin.MuscleMass)/1000.0), - ) - } - fmt.Fprintf(os.Stdout, "\n") - t2.Output(os.Stdout) -} - -func weightGoal(_ *cobra.Command, args []string) { - displayName := "" - - if len(args) > 0 { - displayName = args[0] - } - - goal, err := client.WeightGoal(displayName) - bail(err) - - t := NewTabular() - t.AddValue("ID", goal.ID) - t.AddValue("Created", goal.Created) - t.AddValueUnit("Target", float64(goal.Value)/1000.0, "kg") - t.Output(os.Stdout) -} - -func weightGoalSet(_ *cobra.Command, args []string) { - goal, err := strconv.Atoi(args[0]) - bail(err) - - err = client.SetWeightGoal(goal) - bail(err) -} diff --git a/python-garmin-connect/doc.go b/python-garmin-connect/doc.go deleted file mode 100644 index fdc42fc..0000000 --- a/python-garmin-connect/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package connect provides access to the unofficial Garmin Connect API. This -// is not supported or endorsed by Garmin Ltd. The API may change or stop -// working at any time. Please use responsible. -package connect diff --git a/python-garmin-connect/go.mod b/python-garmin-connect/go.mod deleted file mode 100644 index b4831ad..0000000 --- a/python-garmin-connect/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/abrander/garmin-connect - -go 1.15 - -require ( - github.com/spf13/cobra v1.1.1 - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad -) diff --git a/python-garmin-connect/go.sum b/python-garmin-connect/go.sum deleted file mode 100644 index b346227..0000000 --- a/python-garmin-connect/go.sum +++ /dev/null @@ -1,292 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/python-garmin-connect/tools.go b/python-garmin-connect/tools.go deleted file mode 100644 index 3d6d9c0..0000000 --- a/python-garmin-connect/tools.go +++ /dev/null @@ -1,37 +0,0 @@ -package connect - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "time" -) - -// date formats a time.Time as a date usable in the Garmin Connect API. -func formatDate(t time.Time) string { - return fmt.Sprintf("%04d-%02d-%02d", t.Year(), t.Month(), t.Day()) -} - -// drainBody reads all of b to memory and then returns two equivalent -// ReadClosers yielding the same bytes. -// -// It returns an error if the initial slurp of all bytes fails. It does not attempt -// to make the returned ReadClosers have identical error-matching behavior. -// -// Liberated from net/http/httputil/dump.go. -func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { - if b == http.NoBody { - // No copying needed. Preserve the magic sentinel meaning of NoBody. - return http.NoBody, http.NoBody, nil - } - var buf bytes.Buffer - if _, err = buf.ReadFrom(b); err != nil { - return nil, b, err - } - if err = b.Close(); err != nil { - return nil, b, err - } - return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil -}