From 1a40adb4320e170ba55450f6532993d0cde75753 Mon Sep 17 00:00:00 2001 From: Tom Dudley Date: Sat, 18 Feb 2023 20:17:22 +0000 Subject: [PATCH 1/4] Add WorkoutId to CalendarItem --- Calendar.go | 1 + 1 file changed, 1 insertion(+) diff --git a/Calendar.go b/Calendar.go index 09c4a7f..4126ab0 100644 --- a/Calendar.go +++ b/Calendar.go @@ -63,6 +63,7 @@ type CalendarItem struct { AutoCalcCalories bool `json:"autoCalcCalories"` ProtectedWorkoutSchedule bool `json:"protectedWorkoutSchedule"` IsParent bool `json:"isParent"` + WorkoutId int `json:"workoutId,omitempty"` } // CalendarYear will get the activity summaries and list of days active for a given year From 5a5d4c31d989e2522d354bcfa5fc3bc5b06c058b Mon Sep 17 00:00:00 2001 From: Tom Dudley Date: Sat, 18 Feb 2023 20:26:52 +0000 Subject: [PATCH 2/4] Add workouts --- Client.go | 65 ++++++++++++- Workout.go | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 Workout.go diff --git a/Client.go b/Client.go index b3f62a6..6dd41db 100644 --- a/Client.go +++ b/Client.go @@ -230,9 +230,7 @@ func (c *Client) getJSON(url string, target interface{}) error { 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 { +func (c *Client) writeWithMethodOverride(method string, url string, payload interface{}, expectedStatus int, methodOverride string) error { var body io.Reader if payload != nil { @@ -254,6 +252,10 @@ func (c *Client) write(method string, url string, payload interface{}, expectedS req.Header.Add("content-type", "application/json") } + if methodOverride != "" { + req.Header.Add("X-HTTP-Method-Override", "PUT") + } + resp, err := c.do(req) if err != nil { return err @@ -267,6 +269,63 @@ func (c *Client) write(method string, url string, payload interface{}, expectedS return nil } +func (c *Client) writeAndGetJSON(method string, url string, payload interface{}, expectedStatus int, target interface{}) error { + resp, err := c.writeWithResponse(method, url, payload, expectedStatus) + if err != nil { + return err + } + + decoder := json.NewDecoder(resp.Body) + + return decoder.Decode(target) +} + +func (c *Client) writeWithResponse(method string, url string, payload interface{}, expectedStatus int) (*http.Response, error) { + var body io.Reader + + if payload != nil { + b, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + body = bytes.NewReader(b) + } + + req, err := c.newRequest(method, url, body) + if err != nil { + return nil, 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 nil, err + } + resp.Body.Close() + + return resp, nil +} + +// 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 { + resp, err := c.writeWithResponse(method, url, payload, expectedStatus) + if err != nil { + return err + } + + 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() diff --git a/Workout.go b/Workout.go new file mode 100644 index 0000000..c3134d9 --- /dev/null +++ b/Workout.go @@ -0,0 +1,271 @@ +package connect + +import ( + "encoding/json" + "fmt" +) + +type SportType struct { + SportTypeId int `json:"sportTypeId"` + SportTypeKey string `json:"sportTypeKey"` +} + +type Author struct { + UserProfilePk int `json:"userProfilePk"` + DisplayName string `json:"displayName"` + FullName string `json:"fullName"` + ProfileImgNameLarge string `json:"profileImgNameLarge"` + ProfileImgNameMedium string `json:"profileImgNameMedium"` + ProfileImgNameSmall string `json:"profileImgNameSmall"` + UserPro bool `json:"userPro"` + VivokidUser bool `json:"vivokidUser"` +} + +type EstimatedDistanceUnit struct { + UnitId int `json:"unitId"` + UnitKey string `json:"unitKey"` + Factor float64 `json:"factor"` +} + +type WorkoutSegment struct { + SegmentOrder int `json:"segmentOrder"` + SportType *SportType `json:"sportType"` + WorkoutSteps []WorkoutStep `json:"workoutSteps"` +} + +type StepType struct { + StepTypeId int `json:"stepTypeId"` + StepTypeKey string `json:"stepTypeKey"` +} + +type EndCondition struct { + ConditionTypeId int `json:"conditionTypeId"` + ConditionTypeKey string `json:"conditionTypeKey"` + Displayable bool `json:"displayable"` +} + +type TargetType struct { + WorkoutTargetTypeId int `json:"workoutTargetTypeId"` + WorkoutTargetTypeKey string `json:"workoutTargetTypeKey"` +} + +type PreferredEndConditionUnit struct { + UnitId int `json:"unitId"` + UnitKey string `json:"unitKey"` + Factor float64 `json:"factor"` +} + +type WorkoutStep struct { + Type string `json:"type"` + StepId int `json:"stepId"` + StepOrder int `json:"stepOrder"` + StepType *StepType `json:"stepType"` + ChildStepId int `json:"childStepId"` + Description string `json:"description"` + EndCondition *EndCondition `json:"endCondition"` + EndConditionValue float64 `json:"endConditionValue"` + PreferredEndConditionUnit *PreferredEndConditionUnit `json:"preferredEndConditionUnit,omitempty"` + EndConditionCompare bool `json:"endConditionCompare"` + TargetType *TargetType `json:"targetType"` + TargetValueOne float64 `json:"targetValueOne,omitempty"` + TargetValueTwo float64 `json:"targetValueTwo,omitempty"` + TargetValueUnit string `json:"targetValueUnit,omitempty"` + ZoneNumber int `json:"zoneNumber"` + // Various others.. +} + +// Workout describes a Garmin Connect workout entry +type Workout struct { + WorkoutId int `json:"workoutId"` + WorkoutName string `json:"workoutName"` + OwnerId int `json:"ownerId"` + Description *string `json:"description,omitempty"` + UpdateDate *Time `json:"updateDate,omitempty"` + CreatedDate *Time `json:"createdDate,omitempty"` + SportType *SportType `json:"sportType,omitempty"` + TrainingPlanId *int `json:"trainingPlanId,omitempty"` + Author *Author `json:"author,omitempty"` + EstimatedDurationInSecs *int `json:"estimatedDurationInSecs,omitempty"` + EstimatedDistanceInMeters *float64 `json:"estimatedDistanceInMeters,omitempty"` + WorkoutSegments []*WorkoutSegment `json:"workoutSegments,omitempty"` + EstimateType *string `json:"estimateType,omitempty"` + EstimatedDistanceUnit *EstimatedDistanceUnit `json:"estimatedDistanceUnit,omitempty"` + Locale *string `json:"locale,omitempty"` + WorkoutProvider *string `json:"workoutProvider,omitempty"` + UploadTimestamp *Time `json:"uploadTimestamp,omitempty"` + Consumer *string `json:"consumer,omitempty"` + ConsumerName *string `json:"consumerName,omitempty"` + ConsumerImageUrl *string `json:"consumerImageURL,omitempty"` + ConsumerWebsiteUrl *string `json:"consumerWebsiteURL,omitempty"` + AtpPlanId *int `json:"atpPlanId,omitempty"` + WorkoutNameI18nKey *string `json:"workoutNameI18nKey,omitempty"` + DescriptionI18nKey *string `json:"descriptionI18nKey,omitempty"` + AvgTrainingSpeed *float64 `json:"avgTrainingSpeed,omitempty"` + Shared *bool `json:"shared,omitempty"` +} + +// workoutRequest is the bare minimum required to create a workout +type workoutRequest struct { + SportType *SportType `json:"sportType"` + WorkoutName string `json:"workoutName"` + WorkoutSegments []*WorkoutSegment `json:"workoutSegments"` + AvgTrainingSpeed float64 `json:"avgTrainingSpeed"` + Description string `json:"description,omitempty"` +} + +func (w *Workout) MarshalJSON() ([]byte, error) { + type Alias Workout + + var createdDate string + if w.CreatedDate == nil || w.CreatedDate.IsZero() { + createdDate = "" + } else { + createdDate = w.CreatedDate.Format("2006-01-02T15:04:05.0") + } + + return json.Marshal(&struct { + *Alias + CreatedDate string `json:"createdDate"` + }{ + Alias: (*Alias)(w), + CreatedDate: createdDate, + }) +} + +func (c *Client) Workout() ([]Workout, error) { + URL := "https://connect.garmin.com/modern/proxy/workout-service/workouts" + var workout []Workout + err := c.getJSON(URL, &workout) + if err != nil { + return nil, err + } + + return workout, nil +} + +func (c *Client) GetWorkout(workoutId int) (*Workout, error) { + workout := new(Workout) + URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/workout-service/workout/%d", workoutId) + err := c.getJSON(URL, &workout) + if err != nil { + return nil, err + } + + return workout, nil +} + +func (c *Client) UpdateWorkout(workout *Workout) error { + workout.UpdateDate = nil + + workoutId := workout.WorkoutId + URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/workout-service/workout/%d", workoutId) + err := c.writeWithMethodOverride("POST", URL, workout, 204, "PUT") + if err != nil { + return err + } + + return nil +} + +func (c *Client) CreateWorkout(workout *Workout) (*Workout, error) { + workoutRequest := &workoutRequest{ + SportType: workout.SportType, + WorkoutName: workout.WorkoutName, + WorkoutSegments: workout.WorkoutSegments, + AvgTrainingSpeed: *workout.AvgTrainingSpeed, + Description: *workout.Description, + } + + fmt.Println(workoutRequest) + workoutResponse := new(Workout) + URL := "https://connect.garmin.com/modern/proxy/workout-service/workout" + err := c.writeAndGetJSON("POST", URL, workoutRequest, 200, &workoutResponse) + if err != nil { + return nil, err + } + + return workoutResponse, nil +} + +type WorkoutSchedule struct { + WorkoutScheduleId int `json:"workoutScheduleId"` + Workout Workout `json:"workout"` + CalendarDate string `json:"calendarDate"` + CreatedDate string `json:"createdDate"` + OwnerId int `json:"ownerId"` +} + +type WorkoutSchedulePayload struct { + Date *Time +} + +func (s *WorkoutSchedulePayload) MarshalJSON() ([]byte, error) { + type Alias WorkoutSchedulePayload + + var date string + if s.Date == nil || s.Date.IsZero() { + date = "" + } else { + date = s.Date.Format("2006-01-02") + } + + return json.Marshal(&struct { + *Alias + Date string `json:"date"` + }{ + Alias: (*Alias)(s), + Date: date, + }) +} + +func (c *Client) ScheduleWorkout(workoutId int, date *Time) (*WorkoutSchedule, error) { + workoutSchedule := new(WorkoutSchedule) + URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/workout-service/schedule/%d", workoutId) + + payload := &WorkoutSchedulePayload{Date: date} + + err := c.writeAndGetJSON("POST", URL, payload, 200, &workoutSchedule) + if err != nil { + return nil, err + } + + return workoutSchedule, nil +} + +type WorkoutScheduleSummary struct { + ScheduleId int + WorkoutId int + Title string + Date string +} + +func (c *Client) WorkoutSchedule(year, month int) ([]*WorkoutScheduleSummary, error) { + calendarMonth, err := c.CalendarMonth(year, month) + if err != nil { + return nil, err + } + + var workouts []*WorkoutScheduleSummary + for _, activity := range calendarMonth.CalendarItems { + if activity.ItemType == "workout" { + workouts = append(workouts, &WorkoutScheduleSummary{ + ScheduleId: activity.ID, + WorkoutId: activity.WorkoutId, + Title: activity.Title, + Date: activity.Date.String(), + }) + } + } + + return workouts, nil +} + +func (c *Client) DeleteScheduledWorkout(workoutId int) error { + URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/workout-service/schedule/%d", workoutId) + err := c.write("DELETE", URL, nil, 200) + if err != nil { + return err + } + + return nil +} From 4ab82642d15e46a415c6469d03d2c9b71e97b22a Mon Sep 17 00:00:00 2001 From: Tom Dudley Date: Wed, 22 Feb 2023 19:00:55 +0000 Subject: [PATCH 3/4] Use time.Time --- Workout.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Workout.go b/Workout.go index c3134d9..1e08073 100644 --- a/Workout.go +++ b/Workout.go @@ -3,6 +3,7 @@ package connect import ( "encoding/json" "fmt" + "time" ) type SportType struct { @@ -196,7 +197,7 @@ type WorkoutSchedule struct { } type WorkoutSchedulePayload struct { - Date *Time + Date *time.Time } func (s *WorkoutSchedulePayload) MarshalJSON() ([]byte, error) { @@ -218,7 +219,7 @@ func (s *WorkoutSchedulePayload) MarshalJSON() ([]byte, error) { }) } -func (c *Client) ScheduleWorkout(workoutId int, date *Time) (*WorkoutSchedule, error) { +func (c *Client) ScheduleWorkout(workoutId int, date *time.Time) (*WorkoutSchedule, error) { workoutSchedule := new(WorkoutSchedule) URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/workout-service/schedule/%d", workoutId) From 6831d4c401128688d84da76b05eb810a03fdfcd9 Mon Sep 17 00:00:00 2001 From: Tom Dudley Date: Wed, 22 Feb 2023 19:41:39 +0000 Subject: [PATCH 4/4] Fix nil pointer deref for description --- Workout.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Workout.go b/Workout.go index 1e08073..c661beb 100644 --- a/Workout.go +++ b/Workout.go @@ -174,7 +174,10 @@ func (c *Client) CreateWorkout(workout *Workout) (*Workout, error) { WorkoutName: workout.WorkoutName, WorkoutSegments: workout.WorkoutSegments, AvgTrainingSpeed: *workout.AvgTrainingSpeed, - Description: *workout.Description, + } + + if workout.Description != nil { + workoutRequest.Description = *workout.Description } fmt.Println(workoutRequest) @@ -188,6 +191,16 @@ func (c *Client) CreateWorkout(workout *Workout) (*Workout, error) { return workoutResponse, nil } +func (c *Client) DeleteWorkout(workoutId int) error { + URL := fmt.Sprintf("https://connect.garmin.com/modern/proxy/workout-service/workout/%d", workoutId) + err := c.write("DELETE", URL, nil, 200) + if err != nil { + return err + } + + return nil +} + type WorkoutSchedule struct { WorkoutScheduleId int `json:"workoutScheduleId"` Workout Workout `json:"workout"`