+ var dataPoints []*fitness.DataPoint
+Next:
+ for _, a := range activities {
+ startTimeNanos := a.Start.UnixNano()
+ endTimeNanos := a.End.UnixNano()
+ activityType := ParseFitbitActivity(a.Type)
+
+ for _, p := range dataset.Point {
+ if p.StartTimeNanos == startTimeNanos && p.EndTimeNanos == endTimeNanos && p.Value[0].IntVal == activityType {
+ log.Debugf(ctx, "activity %s already stored in Google Fit", a)
+ continue Next
+ }
+ }
+
+ log.Debugf(ctx, "activity %s will be added to Google Fit", a)
+ dataPoints = append(dataPoints, &fitness.DataPoint{
+ DataTypeName: dataTypeNameActivitySegment,
+ StartTimeNanos: startTimeNanos,
+ EndTimeNanos: endTimeNanos,
+ Value: []*fitness.Value{
+ &fitness.Value{IntVal: activityType},
+ },
+ })
+ }
+
+ if len(dataPoints) == 0 {
+ log.Debugf(ctx, "SetActivities(): len(dataPoints) == 0")
+ return nil
+ }
+
+ log.Debugf(ctx, "SetActivities(): calling c.DatasetPatch(%q)", dataStreamID)
+ return c.DatasetPatch(ctx, dataStreamID, dataPoints)
+}
+
+func (c *Client) updateIncremental(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)
+ storedValue, startTime, err := c.readIncremental(ctx, dataSource, startOfDay, endOfDay)
+ if err != nil {
+ return err
+ }
+
+ var diffValue fitness.Value
+ if dataSource.DataType.Field[0].Format == "integer" {
+ if storedValue.IntVal > rawValue.IntVal {
+ log.Warningf(ctx, "stored value (%d) is larger than new value (%d)", storedValue.IntVal, rawValue.IntVal)
+ return nil
+ }
+ if rawValue.IntVal == storedValue.IntVal {
+ return nil
+ }
+ diffValue.IntVal = rawValue.IntVal - storedValue.IntVal
+ } else { // if dataSource.DataType.Field[0].Format == "floatPoint"
+ if storedValue.FpVal > rawValue.FpVal {
+ log.Warningf(ctx, "stored value (%g) is larger than new value (%g)", storedValue.FpVal, rawValue.FpVal)
+ return nil
+ }
+ if rawValue.FpVal == storedValue.FpVal {
+ return nil
+ }
+ diffValue.FpVal = rawValue.FpVal - storedValue.FpVal
+ }
+
+ endTime := endOfDay
+ if now := time.Now().In(startOfDay.Location()); now.Before(endOfDay) {
+ endTime = now
+ }
+ log.Debugf(ctx, "add cumulative data %s until %v: %+v", dataSource.DataStreamId, 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) readIncremental(ctx context.Context, dataSource *fitness.DataSource, startTime, endTime time.Time) (*fitness.Value, time.Time, error) {
+ dataset, err := c.DatasetGet(ctx, dataSource.DataStreamId, startTime, endTime)
+ if err != nil {
+ return nil, time.Time{}, err
+ }
+
+ if len(dataset.Point) == 0 {
+ log.Debugf(ctx, "read cumulative data %s until %v: []", dataSource.DataStreamId, endTime)
+ return &fitness.Value{}, startTime, nil
+ }
+
+ var sum fitness.Value
+ maxEndTime := startTime
+ for _, p := range dataset.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, "read cumulative data %s until %v: %+v", dataSource.DataStreamId, maxEndTime, sum)
+ return &sum, maxEndTime, nil
+}
+
+type heartRateDuration struct {
+ Min int
+ Max int
+ Duration time.Duration
+}
+
+type heartRateDurations []*heartRateDuration
+
+func (res heartRateDurations) find(min, max int) (*heartRateDuration, bool) {
+ for _, d := range res {
+ if d.Min != min || d.Max != max {
+ continue
+ }
+ return d, true
+ }
+
+ return nil, false
+}
+
+func (c *Client) heartRate(ctx context.Context, dataSource *fitness.DataSource, startTime, endTime time.Time) (heartRateDurations, time.Time, error) {
+ dataset, err := c.DatasetGet(ctx, dataSource.DataStreamId, startTime, endTime)
+ if err != nil {
+ return nil, time.Time{}, err
+ }
+
+ if len(dataset.Point) == 0 {
+ return nil, startTime, nil
+ }
+
+ var results heartRateDurations
+ maxEndTime := startTime
+ for _, p := range dataset.Point {
+ max := int(p.Value[1].FpVal)
+ min := int(p.Value[2].FpVal)
+ duration := time.Unix(0, p.EndTimeNanos).Sub(time.Unix(0, p.StartTimeNanos))
+
+ if d, ok := results.find(min, max); ok {
+ d.Duration += duration
+ } else {
+ results = append(results, &heartRateDuration{
+ Min: min,
+ Max: max,
+ Duration: duration,
+ })
+ }
+
+ pointEndTime := time.Unix(0, p.EndTimeNanos).In(startTime.Location())
+ if maxEndTime.Before(pointEndTime) {
+ maxEndTime = pointEndTime
+ }
+ }
+
+ return results, maxEndTime, nil
+}
+
+func (c *Client) SetHeartRate(ctx context.Context, totalDurations []fitbit.HeartRateZone, restingHeartRate int, startOfDay time.Time) error {
+ dataSource := &fitness.DataSource{
+ Application: Application(ctx),
+ DataType: &fitness.DataType{
+ Field: []*fitness.DataTypeField{
+ &fitness.DataTypeField{
+ Name: "average",
+ Format: "floatPoint",
+ },
+ &fitness.DataTypeField{
+ Name: "max",
+ Format: "floatPoint",
+ },
+ &fitness.DataTypeField{
+ Name: "min",
+ Format: "floatPoint",