Distance float64 `json:"distance"`
}
+type HeartRateZone struct {
+ Name string `json:"name"`
+ Min int `json:"min"`
+ Max int `json:"max"`
+ Minutes int `json:"minutes"`
+ CaloriesOut float64 `json:"caloriesOut"`
+}
+
type ActivitySummary struct {
Activities []Activity `json:"activities"`
Goals struct {
Steps int `json:"steps"`
} `json:"goals"`
Summary struct {
- ActivityCalories int `json:"activityCalories"`
- CaloriesBMR int `json:"caloriesBMR"`
- CaloriesOut int `json:"caloriesOut"`
- MarginalCalories int `json:"marginalCalories"`
- Distances []Distance `json:"distances"`
- Elevation float64 `json:"elevation"`
- Floors int `json:"floors"`
- Steps int `json:"steps"`
- SedentaryMinutes int `json:"sedentaryMinutes"`
- LightlyActiveMinutes int `json:"lightlyActiveMinutes"`
- FairlyActiveMinutes int `json:"fairlyActiveMinutes"`
- VeryActiveMinutes int `json:"veryActiveMinutes"`
+ ActiveScore int `json:"activeScore"`
+ ActivityCalories int `json:"activityCalories"`
+ CaloriesBMR int `json:"caloriesBMR"`
+ CaloriesOut float64 `json:"caloriesOut"`
+ Distances []Distance `json:"distances"`
+ Elevation float64 `json:"elevation"`
+ Floors int `json:"floors"`
+ HeartRateZones []HeartRateZone `json:"heartRateZones"`
+ MarginalCalories int `json:"marginalCalories"`
+ RestingHeartRate int `json:"restingHeartRate"`
+ Steps int `json:"steps"`
+ SedentaryMinutes int `json:"sedentaryMinutes"`
+ LightlyActiveMinutes int `json:"lightlyActiveMinutes"`
+ FairlyActiveMinutes int `json:"fairlyActiveMinutes"`
+ VeryActiveMinutes int `json:"veryActiveMinutes"`
} `json:"summary"`
}
csrfToken = "@CSRFTOKEN@"
userID = "me"
- dataTypeNameSteps = "com.google.step_count.delta"
+ dataTypeNameCalories = "com.google.calories.expended"
+ dataTypeNameSteps = "com.google.step_count.delta"
)
var oauthConfig = &oauth2.Config{
},
})
}
+
+func (c *Client) SetCalories(ctx context.Context, totalCalories float64, startOfDay time.Time) error {
+ return c.updateCumulative(ctx,
+ &fitness.DataSource{
+ Application: Application(ctx),
+ DataType: &fitness.DataType{
+ Field: []*fitness.DataTypeField{
+ &fitness.DataTypeField{
+ Name: "calories",
+ Format: "floatPoint",
+ },
+ },
+ Name: dataTypeNameCalories,
+ },
+ Name: "Calories expended",
+ Type: "raw",
+ },
+ &fitness.Value{
+ FpVal: totalCalories,
+ },
+ startOfDay)
+}
+
+func (c *Client) updateCumulative(ctx context.Context, dataSource *fitness.DataSource, rawValue *fitness.Value, startOfDay time.Time) error {
+ switch f := dataSource.DataType.Field[0].Format; f {
+ case "integer":
+ if rawValue.IntVal == 0 {
+ return nil
+ }
+ case "floatPoint":
+ if rawValue.FpVal == 0 {
+ return nil
+ }
+ default:
+ return fmt.Errorf("unexpected data type field format %q", f)
+ }
+
+ dataSourceID, err := c.DataSourceCreate(ctx, dataSource)
+ if err != nil {
+ return err
+ }
+ dataSource.DataStreamId = dataSourceID
+
+ endOfDay := startOfDay.Add(24 * time.Hour).Add(-1 * time.Nanosecond)
+ currValue, startTime, err := c.readCumulative(ctx, dataSource, startOfDay, endOfDay)
+
+ var diffValue fitness.Value
+ if dataSource.DataType.Field[0].Format == "integer" {
+ if rawValue.IntVal == currValue.IntVal {
+ return nil
+ }
+ diffValue.IntVal = rawValue.IntVal - currValue.IntVal
+ if diffValue.IntVal < 0 {
+ log.Warningf(ctx, "stored value (%d) is larger than new value (%d); assuming count was reset", currValue.IntVal, rawValue.IntVal)
+ diffValue.IntVal = rawValue.IntVal
+ }
+ } else { // if dataSource.DataType.Field[0].Format == "floatPoint"
+ if rawValue.FpVal == currValue.FpVal {
+ return nil
+ }
+ diffValue.FpVal = rawValue.FpVal - currValue.FpVal
+ if diffValue.FpVal < 0 {
+ log.Warningf(ctx, "stored value (%g) is larger than new value (%g); assuming count was reset", currValue.FpVal, rawValue.FpVal)
+ diffValue.FpVal = rawValue.FpVal
+ }
+ }
+
+ endTime := endOfDay
+ if now := time.Now().In(startOfDay.Location()); now.Before(endOfDay) {
+ endTime = now
+ }
+ log.Debugf(ctx, "new cumulative data point: [%v--%v] %+v", startTime, endTime, diffValue)
+
+ return c.DataSetPatch(ctx, dataSource.DataStreamId, []*fitness.DataPoint{
+ &fitness.DataPoint{
+ DataTypeName: dataSource.DataType.Name,
+ StartTimeNanos: startTime.UnixNano(),
+ EndTimeNanos: endTime.UnixNano(),
+ Value: []*fitness.Value{&diffValue},
+ },
+ })
+}
+
+func (c *Client) readCumulative(ctx context.Context, dataSource *fitness.DataSource, startTime, endTime time.Time) (*fitness.Value, time.Time, error) {
+ datasetID := fmt.Sprintf("%d-%d", startTime.UnixNano(), endTime.UnixNano())
+
+ res, err := c.Service.Users.DataSources.Datasets.Get(userID, dataSource.DataStreamId, datasetID).Context(ctx).Do()
+ if err != nil {
+ log.Errorf(ctx, "c.Service.Users.DataSources.Datasets.Get(%q, %q) = %v", dataSource.DataStreamId, datasetID, err)
+ return nil, time.Time{}, err
+ }
+
+ if len(res.Point) == 0 {
+ return &fitness.Value{}, startTime, nil
+ }
+
+ var sum fitness.Value
+ maxEndTime := startTime
+ for _, p := range res.Point {
+ switch f := dataSource.DataType.Field[0].Format; f {
+ case "integer":
+ sum.IntVal += p.Value[0].IntVal
+ case "floatPoint":
+ sum.FpVal += p.Value[0].FpVal
+ default:
+ return nil, time.Time{}, fmt.Errorf("unexpected data type field format %q", f)
+ }
+
+ pointEndTime := time.Unix(0, p.EndTimeNanos).In(startTime.Location())
+ if maxEndTime.Before(pointEndTime) {
+ maxEndTime = pointEndTime
+ }
+ }
+
+ log.Debugf(ctx, "Google Fit has data points for %s until %v: %+v", dataSource.DataStreamId, maxEndTime, sum)
+ return &sum, maxEndTime, nil
+}