This commit is contained in:
2025-09-21 11:03:52 -07:00
parent 667790030e
commit e04cd5160e
138 changed files with 17338 additions and 0 deletions

View File

@@ -0,0 +1 @@
/connect

View File

@@ -0,0 +1 @@
This is a simple CLI client for Garmin Connect.

View File

@@ -0,0 +1,81 @@
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)
}
}

View File

@@ -0,0 +1,63 @@
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())
}
}

View File

@@ -0,0 +1,217 @@
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 <activity id>",
Short: "View details for an activity",
Run: activitiesView,
Args: cobra.ExactArgs(1),
}
activitiesCmd.AddCommand(activitiesViewCmd)
activitiesViewWeatherCmd := &cobra.Command{
Use: "weather <activity id>",
Short: "View weather for an activity",
Run: activitiesViewWeather,
Args: cobra.ExactArgs(1),
}
activitiesViewCmd.AddCommand(activitiesViewWeatherCmd)
activitiesViewHRZonesCmd := &cobra.Command{
Use: "hrzones <activity id>",
Short: "View hr zones for an activity",
Run: activitiesViewHRZones,
Args: cobra.ExactArgs(1),
}
activitiesViewCmd.AddCommand(activitiesViewHRZonesCmd)
activitiesExportCmd := &cobra.Command{
Use: "export <activity id>",
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 <path>",
Short: "Import an activity from a file",
Run: activitiesImport,
Args: cobra.ExactArgs(1),
}
activitiesCmd.AddCommand(activitiesImportCmd)
activitiesDeleteCmd := &cobra.Command{
Use: "delete <activity id>",
Short: "Delete an activity",
Run: activitiesDelete,
Args: cobra.ExactArgs(1),
}
activitiesCmd.AddCommand(activitiesDeleteCmd)
activitiesRenameCmd := &cobra.Command{
Use: "rename <activity id> <new name>",
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)
}

View File

@@ -0,0 +1,222 @@
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 <badge id>",
Short: "Show details about a badge",
Run: badgesView,
Args: cobra.ExactArgs(1),
}
badgesCmd.AddCommand(badgesViewCmd)
badgesCompareCmd := &cobra.Command{
Use: "compare <display name>",
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)
}

View File

@@ -0,0 +1,114 @@
package main
import (
"os"
"strconv"
"github.com/spf13/cobra"
)
func init() {
calendarCmd := &cobra.Command{
Use: "calendar",
}
rootCmd.AddCommand(calendarCmd)
calendarYearCmd := &cobra.Command{
Use: "year <year>",
Short: "List active days in the year",
Run: calendarYear,
Args: cobra.RangeArgs(1, 1),
}
calendarCmd.AddCommand(calendarYearCmd)
calendarMonthCmd := &cobra.Command{
Use: "month <year> <month>",
Short: "List active days in the month",
Run: calendarMonth,
Args: cobra.RangeArgs(2, 2),
}
calendarCmd.AddCommand(calendarMonthCmd)
calendarWeekCmd := &cobra.Command{
Use: "week <year> <month> <day>",
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)
}

View File

@@ -0,0 +1,169 @@
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 <invation ID>",
Short: "Accept an ad-hoc challenge",
Run: challengesAccept,
Args: cobra.ExactArgs(1),
}
challengesCmd.AddCommand(challengesAcceptCmd)
challengesDeclineCmd := &cobra.Command{
Use: "decline <invation ID>",
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 <id>",
Short: "View challenge details",
Run: challengesView,
Args: cobra.ExactArgs(1),
}
challengesCmd.AddCommand(challengesViewCmd)
challengesLeaveCmd := &cobra.Command{
Use: "leave <challenge id>",
Short: "Leave a challenge",
Run: challengesLeave,
Args: cobra.ExactArgs(1),
}
challengesCmd.AddCommand(challengesLeaveCmd)
challengesRemoveCmd := &cobra.Command{
Use: "remove <challenge id> <user id>",
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)
}

View File

@@ -0,0 +1,38 @@
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)
}

View File

@@ -0,0 +1,180 @@
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 <connection ID>",
Short: "Remove a connection",
Run: connectionsRemove,
Args: cobra.ExactArgs(1),
}
connectionsCmd.AddCommand(connectionsRemoveCmd)
connectionsSearchCmd := &cobra.Command{
Use: "search <keyword>",
Short: "Search Garmin wide for a person",
Run: connectionsSearch,
Args: cobra.ExactArgs(1),
}
connectionsCmd.AddCommand(connectionsSearchCmd)
connectionsAcceptCmd := &cobra.Command{
Use: "accept <request id>",
Short: "Accept a connection request",
Run: connectionsAccept,
Args: cobra.ExactArgs(1),
}
connectionsCmd.AddCommand(connectionsAcceptCmd)
connectionsRequestCmd := &cobra.Command{
Use: "request <display name>",
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 <display name>",
Short: "Add a user to the blocked list",
Run: connectionsBlockedAdd,
Args: cobra.ExactArgs(1),
}
blockedCmd.AddCommand(blockedAddCmd)
blockedRemoveCmd := &cobra.Command{
Use: "remove <display name>",
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)
}

View File

@@ -0,0 +1,151 @@
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 <gear UUID> <activity id>",
Short: "Link Gear to Activity",
Run: gearLink,
Args: cobra.ExactArgs(2),
}
gearCmd.AddCommand(gearLinkCommand)
gearUnlinkCommand := &cobra.Command{
Use: "unlink <gear UUID> <activity id>",
Short: "Unlink Gear to Activity",
Run: gearUnlink,
Args: cobra.ExactArgs(2),
}
gearCmd.AddCommand(gearUnlinkCommand)
gearForActivityCommand := &cobra.Command{
Use: "activity <activity id>",
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)
}

View File

@@ -0,0 +1,46 @@
package main
import (
"bytes"
"encoding/json"
"io"
"os"
"github.com/spf13/cobra"
)
var (
formatJSON bool
)
func init() {
getCmd := &cobra.Command{
Use: "get <URL>",
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)
}
}

View File

@@ -0,0 +1,67 @@
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 <goal id>",
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)
}

View File

@@ -0,0 +1,189 @@
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 <group id>",
Short: "View group details",
Run: groupsView,
Args: cobra.ExactArgs(1),
}
groupsCmd.AddCommand(groupsViewCmd)
groupsViewAnnouncementCmd := &cobra.Command{
Use: "announcement <group id>",
Short: "View group abbouncement",
Run: groupsViewAnnouncement,
Args: cobra.ExactArgs(1),
}
groupsViewCmd.AddCommand(groupsViewAnnouncementCmd)
groupsViewMembersCmd := &cobra.Command{
Use: "members <group id>",
Short: "View group members",
Run: groupsViewMembers,
Args: cobra.ExactArgs(1),
}
groupsViewCmd.AddCommand(groupsViewMembersCmd)
groupsSearchCmd := &cobra.Command{
Use: "search <keyword>",
Short: "Search for a group",
Run: groupsSearch,
Args: cobra.ExactArgs(1),
}
groupsCmd.AddCommand(groupsSearchCmd)
groupsJoinCmd := &cobra.Command{
Use: "join <group id>",
Short: "Join a group",
Run: groupsJoin,
Args: cobra.ExactArgs(1),
}
groupsCmd.AddCommand(groupsJoinCmd)
groupsLeaveCmd := &cobra.Command{
Use: "leave <group id>",
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)
}

View File

@@ -0,0 +1,96 @@
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)
}

View File

@@ -0,0 +1,96 @@
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 = ""
}

View File

@@ -0,0 +1,16 @@
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 "-"
}

View File

@@ -0,0 +1,62 @@
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 <date> [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)
}

View File

@@ -0,0 +1,57 @@
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())
}
}

View File

@@ -0,0 +1,70 @@
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)
}

View File

@@ -0,0 +1,224 @@
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 <yyyy-mm-dd> <weight in grams>",
Short: "Add a simple weight for a specific date",
Run: weightAdd,
Args: cobra.ExactArgs(2),
}
weightCmd.AddCommand(weightAddCmd)
weightDeleteCmd := &cobra.Command{
Use: "delete <yyyy-mm-dd]>",
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)
}