From 621249566a571a743165d02f8725d54ba520ec5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Uhrbach?= Date: Sat, 9 Nov 2024 21:12:44 +0100 Subject: [PATCH] Added exercise metrics --- README.md | 12 +++ internal/egym/client.go | 11 ++- internal/egym/workouts.go | 94 +++++++++++++++++++ internal/exporter/exercises.go | 165 +++++++++++++++++++++++++++++++++ internal/exporter/exporter.go | 2 + 5 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 internal/egym/workouts.go create mode 100644 internal/exporter/exercises.go diff --git a/README.md b/README.md index 1322ebf..19f08ca 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,21 @@ docker run \ |egym_strength|Strength metrics about various muscles and muscle groups.| |egym_flexibility|Flexibility metrics about different body parts like neck or hips.| |egym_muscle_imbalance|Proportions and (im)balances of different muscle pairs.| +|egym_exercise_activity_points|Collected activity points of a specific exercise| +|egym_exercise_activity_distance|Distance left behind with the specific exercise| +|egym_exercise_activity_duration|Duration of a specific exercise| +|egym_exercise_activity_calories|Burned calories with a specific exercise| +|egym_exercise_average_speed|Average speed within a specific exercise| +|egym_exercise_sets|Number of sets within a specific exercise| +|egym_exercise_reps|number of reps over all sets of a specific exercise| +|egym_exercise_weight_total|Total weight across all reps of all sets of the exercise| ## Release Notes +### 0.7.0 + +Added metrics about exercises. + ### 0.6.0 Added muscle imbalance metrics. diff --git a/internal/egym/client.go b/internal/egym/client.go index 502a2ef..733eebe 100644 --- a/internal/egym/client.go +++ b/internal/egym/client.go @@ -20,8 +20,8 @@ type EgymClient struct { userId string cookies string defaultHeaders map[string]string - loginUrl string apiUrl string + brandApiUrl string httpClient *http.Client } @@ -42,9 +42,9 @@ func NewEgymClient(brand, username, password string) (*EgymClient, error) { "x-np-app-version": "3.11", "Accept": "application/json", }, - loginUrl: fmt.Sprintf("https://%s.netpulse.com/np/exerciser/login", brand), - apiUrl: "https://mobile-api.int.api.egym.com", - httpClient: httpClient, + brandApiUrl: fmt.Sprintf("https://%s.netpulse.com", brand), + apiUrl: "https://mobile-api.int.api.egym.com", + httpClient: httpClient, } loggedIn, err := c.login() if err != nil || !loggedIn { @@ -62,7 +62,8 @@ func (c *EgymClient) login() (bool, error) { hasLogin := c.userId != "" data.Set("relogin", fmt.Sprintf("%t", hasLogin)) - req, err := http.NewRequest("POST", c.loginUrl, strings.NewReader(data.Encode())) + loginUrl := fmt.Sprintf("%s/np/exerciser/login", c.brandApiUrl) + req, err := http.NewRequest("POST", loginUrl, strings.NewReader(data.Encode())) if err != nil { return false, err } diff --git a/internal/egym/workouts.go b/internal/egym/workouts.go new file mode 100644 index 0000000..0d4095f --- /dev/null +++ b/internal/egym/workouts.go @@ -0,0 +1,94 @@ +package egym + +import ( + "encoding/json" + "fmt" + "time" +) + +func (c *EgymClient) GetWorkoutsInPeriod(startDate, endDate time.Time) (*[]Workout, error) { + url := fmt.Sprintf( + "%s/workouts/api/workouts/v2.3/exercisers/%s/workouts?completedAfter=%s&completedBefore=%s", + c.brandApiUrl, + c.userId, + startDate.Format(time.RFC3339), + endDate.Format(time.RFC3339), + ) + body, err := c.fetch(url, 1) + if err != nil { + return nil, err + } + + var response GetWorkoutsResponse + err = json.Unmarshal(body, &response) + if err != nil { + return nil, err + } + return &response.Workouts, nil +} + +type GetWorkoutsResponse struct { + Workouts []Workout `json:"workouts"` +} + +type Workout struct { + Code string `json:"code"` + Exercises []Exercise `json:"exercises"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + CompletedAt string `json:"completedAt"` + Timezone string `json:"timezone"` + WorkoutPlanCode string `json:"workoutPlanCode"` + WorkoutPlanLabel string `json:"workoutPlanLabel"` + WorkoutPlanImageUrl string `json:"workoutPlanImageUrl"` + WorkoutPlanGroupType string `json:"workoutPlanGroupType"` +} + +type Exercise struct { + Code string `json:"code"` + ExerciseCode string `json:"exerciseCode"` + LibraryCode string `json:"libraryCode"` + Source struct { + Label string `json:"label"` + Code string `json:"code"` + } `json:"source"` + Exercise struct { + Label string `json:"label"` + Code string `json:"code"` + Icons []string `json:"icons"` + Videos []string `json:"videos"` + Previews []string `json:"previews"` + Description string `json:"description"` + Synonyms []string `json:"synonyms"` + Category struct { + Code string `json:"code"` + Label string `json:"label"` + } `json:"category"` + MachineBased bool `json:"machineBased"` + } `json:"exercise"` + Attributes struct { + Distance *ValueWithUnit `json:"distance"` + Duration *ValueWithUnit `json:"duration"` + Calories *ValueWithUnit `json:"calories"` + ActivityPoints *ValueWithUnit `json:"activity_points"` + AverageSpeed *ValueWithUnit `json:"average_speed"` + Sets []struct { + Reps *ValueWithUnit `json:"reps"` + Duration *ValueWithUnit `json:"duration"` + Weight *ValueWithUnit `json:"weight"` + } `json:"sets_of_reps_and_weight_or_duration_and_weight"` + } `json:"attributes"` + Name string `json:"name"` + Editable bool `json:"editable"` + Deletable bool `json:"deletable"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + CompletedAt string `json:"completedAt"` + Timezone string `json:"timezone"` + Origin string `json:"origin"` +} + +type ValueWithUnit struct { + Unit string `json:"unit"` + Value float64 `json:"value"` +} diff --git a/internal/exporter/exercises.go b/internal/exporter/exercises.go new file mode 100644 index 0000000..8ea0039 --- /dev/null +++ b/internal/exporter/exercises.go @@ -0,0 +1,165 @@ +package exporter + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/soerenuhrbach/egym-exporter/internal/egym" + + "time" + + log "github.com/sirupsen/logrus" +) + +const exerciseNamespace = "exercise" + +var ( + exerciseLabels = []string{"exercise", "code", "completed_at", "source", "source_label", "unit", "workout"} + + exerciseActivityPoints = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "activity_points"), + "Activity points of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseDistance = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "distance"), + "Distance of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseDuration = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "duration"), + "Training duration of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseCalories = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "calories"), + "Burned calories with the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseAverageSpeed = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "average_speed"), + "Average speed of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseSets = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "sets"), + "Number of sets of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseReps = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "reps"), + "Total number of reps over all sets of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseWeightPerRep = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "weight_per_rep"), + "Average weight per rep of the exercise", + append(labels, exerciseLabels...), + nil, + ) + exerciseTotalWeight = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exerciseNamespace, "weight_total"), + "Total weight across all reps of all sets of the exercise", + append(labels, exerciseLabels...), + nil, + ) +) + +func (c *EgymExporter) describeExerciseMetrics(ch chan<- *prometheus.Desc) { + ch <- exerciseActivityPoints +} + +func (c *EgymExporter) collectExerciseMetrics(ch chan<- prometheus.Metric) { + now := time.Now().UTC() + today := time.Date( + now.Year(), + now.Month(), + now.Day(), + int(0), + int(0), + int(0), + int(0), + now.Location(), + ) + endOfDay := time.Date( + now.Year(), + now.Month(), + now.Day(), + int(23), + int(59), + int(59), + int(59), + now.Location(), + ) + + workouts, err := c.client.GetWorkoutsInPeriod(today, endOfDay) + if err != nil { + log.Error("could not retrieve exercise data!", err) + return + } + + for _, workout := range *workouts { + for _, exercise := range workout.Exercises { + + if exercise.Attributes.ActivityPoints != nil && exercise.Attributes.ActivityPoints.Value > 0 { + ch <- c.createExerciseMetric(exerciseActivityPoints, workout, exercise, exercise.Attributes.ActivityPoints.Value, exercise.Attributes.ActivityPoints.Unit) + } + if exercise.Attributes.Distance != nil && exercise.Attributes.Distance.Value > 0 { + ch <- c.createExerciseMetric(exerciseDistance, workout, exercise, exercise.Attributes.Distance.Value, exercise.Attributes.Distance.Unit) + } + if exercise.Attributes.Duration != nil && exercise.Attributes.Duration.Value > 0 { + ch <- c.createExerciseMetric(exerciseDuration, workout, exercise, exercise.Attributes.Duration.Value, exercise.Attributes.Duration.Unit) + } + if exercise.Attributes.Calories != nil && exercise.Attributes.Calories.Value > 0 { + ch <- c.createExerciseMetric(exerciseCalories, workout, exercise, exercise.Attributes.Calories.Value, exercise.Attributes.Calories.Unit) + } + if exercise.Attributes.AverageSpeed != nil && exercise.Attributes.AverageSpeed.Value > 0 { + ch <- c.createExerciseMetric(exerciseAverageSpeed, workout, exercise, exercise.Attributes.AverageSpeed.Value, exercise.Attributes.AverageSpeed.Unit) + } + + if exercise.Attributes.Sets != nil && len(exercise.Attributes.Sets) > 0 { + sets := float64(len(exercise.Attributes.Sets)) + reps := float64(0) + weight := float64(0) + + for _, set := range exercise.Attributes.Sets { + if set.Reps != nil { + reps += set.Reps.Value + } + + if set.Weight != nil { + weight += set.Weight.Value + } + } + + weight = weight / sets + + ch <- c.createExerciseMetric(exerciseSets, workout, exercise, sets, "") + ch <- c.createExerciseMetric(exerciseReps, workout, exercise, reps, "") + ch <- c.createExerciseMetric(exerciseWeightPerRep, workout, exercise, weight, "kg") + ch <- c.createExerciseMetric(exerciseTotalWeight, workout, exercise, weight*reps, "kg") + } + } + } +} + +func (c *EgymExporter) createExerciseMetric(desc *prometheus.Desc, workout egym.Workout, exercise egym.Exercise, value float64, unit string) prometheus.Metric { + return prometheus.MustNewConstMetric( + desc, + prometheus.CounterValue, + value, + c.client.Username, + exercise.Exercise.Label, + exercise.Exercise.Code, + exercise.CompletedAt, + exercise.Source.Code, + exercise.Source.Label, + unit, + workout.Code, + ) +} diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go index c119a90..3da7dbe 100644 --- a/internal/exporter/exporter.go +++ b/internal/exporter/exporter.go @@ -22,6 +22,7 @@ func (e *EgymExporter) Describe(ch chan<- *prometheus.Desc) { e.describeStrengthMetrics(ch) e.describeFlexibilityMetrics(ch) e.describeMuscleImbalanceMetrics(ch) + e.describeExerciseMetrics(ch) } func (e *EgymExporter) Collect(ch chan<- prometheus.Metric) { @@ -31,6 +32,7 @@ func (e *EgymExporter) Collect(ch chan<- prometheus.Metric) { e.collectStrengthMetrics(ch) e.collectFlexibilityMetrics(ch) e.collectMuscleImbalanceMetrics(ch) + e.collectExerciseMetrics(ch) } func NewEgymExporter(client *egym.EgymClient) *EgymExporter {