X-Git-Url: https://git.octo.it/?a=blobdiff_plain;ds=sidebyside;f=gfit%2Fgfit.go;h=3e406b4d2824c6d3712ef7d21f2b210c11868b32;hb=a05bb612a8b3c7b66fc6f841040c69b87350e63e;hp=33e5a664f69855806353be662b509933c6a3e179;hpb=3313c545eece0a29c49e7e1621e491d88f2f957b;p=kraftakt.git diff --git a/gfit/gfit.go b/gfit/gfit.go index 33e5a66..3e406b4 100644 --- a/gfit/gfit.go +++ b/gfit/gfit.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/octo/gfitsync/app" - "github.com/octo/gfitsync/fitbit" + "github.com/octo/kraftakt/app" + "github.com/octo/kraftakt/fitbit" "golang.org/x/oauth2" oauth2google "golang.org/x/oauth2/google" fitness "google.golang.org/api/fitness/v1" @@ -18,43 +18,47 @@ import ( ) const ( - csrfToken = "@CSRFTOKEN@" - userID = "me" + userID = "me" - dataTypeNameCalories = "com.google.calories.expended" - dataTypeNameSteps = "com.google.step_count.delta" - dataTypeNameHeartrate = "com.google.heart_rate.summary" + dataTypeNameCalories = "com.google.calories.expended" + dataTypeNameDistance = "com.google.distance.delta" + dataTypeNameSteps = "com.google.step_count.delta" + dataTypeNameHeartrate = "com.google.heart_rate.summary" + dataTypeNameActivitySegment = "com.google.activity.segment" ) -var oauthConfig = &oauth2.Config{ - ClientID: "@GOOGLE_CLIENT_ID@", - ClientSecret: "@GOOGLE_CLIENT_SECRET@", - Endpoint: oauth2google.Endpoint, - RedirectURL: "https://kraftakt.octo.it/google/grant", - Scopes: []string{ - fitness.FitnessActivityWriteScope, - fitness.FitnessBodyWriteScope, - }, +func oauthConfig() *oauth2.Config { + return &oauth2.Config{ + ClientID: app.Config.GoogleClientID, + ClientSecret: app.Config.GoogleClientSecret, + Endpoint: oauth2google.Endpoint, + RedirectURL: "https://kraftakt.octo.it/google/grant", + Scopes: []string{ + fitness.FitnessActivityWriteScope, + fitness.FitnessBodyWriteScope, + fitness.FitnessLocationWriteScope, + }, + } +} + +func AuthURL(ctx context.Context, u *app.User) string { + return oauthConfig().AuthCodeURL(u.Sign("Google"), oauth2.AccessTypeOffline) } func Application(ctx context.Context) *fitness.Application { return &fitness.Application{ - Name: "Fitbit to Google Fit sync", + Name: "Kraftakt", Version: appengine.VersionID(ctx), DetailsUrl: "", // optional } } -func AuthURL() string { - return oauthConfig.AuthCodeURL(csrfToken, oauth2.AccessTypeOffline) -} - func ParseToken(ctx context.Context, r *http.Request, u *app.User) error { - if state := r.FormValue("state"); state != csrfToken { + if state := r.FormValue("state"); state != u.Sign("Google") { return fmt.Errorf("invalid state parameter: %q", state) } - tok, err := oauthConfig.Exchange(ctx, r.FormValue("code")) + tok, err := oauthConfig().Exchange(ctx, r.FormValue("code")) if err != nil { return err } @@ -64,10 +68,11 @@ func ParseToken(ctx context.Context, r *http.Request, u *app.User) error { type Client struct { *fitness.Service + appUser *app.User } func NewClient(ctx context.Context, u *app.User) (*Client, error) { - c, err := u.OAuthClient(ctx, "Google", oauthConfig) + c, err := u.OAuthClient(ctx, "Google", oauthConfig()) if err != nil { return nil, err } @@ -79,14 +84,19 @@ func NewClient(ctx context.Context, u *app.User) (*Client, error) { return &Client{ Service: service, + appUser: u, }, nil } +func (c *Client) DeleteToken(ctx context.Context) error { + return c.appUser.DeleteToken(ctx, "Google") +} + func DataStreamID(dataSource *fitness.DataSource) string { fields := []string{ dataSource.Type, dataSource.DataType.Name, - "@PROJECT_NUMBER@", // FIXME + app.Config.ProjectNumber, } if dev := dataSource.Device; dev != nil { @@ -101,6 +111,10 @@ func DataStreamID(dataSource *fitness.DataSource) string { } } + if dataSource.DataStreamName != "" { + fields = append(fields, dataSource.DataStreamName) + } + return strings.Join(fields, ":") } @@ -113,13 +127,23 @@ func (c *Client) DataSourceCreate(ctx context.Context, dataSource *fitness.DataS } return DataStreamID(dataSource), nil } - log.Errorf(ctx, "c.Service.Users.DataSources.Create() = (%+v, %v)", res, err) - return "", err + return "", fmt.Errorf("DataSources.Create(%q) = %v", DataStreamID(dataSource), err) } + return res.DataStreamId, nil } -func (c *Client) DataSetPatch(ctx context.Context, dataSourceID string, points []*fitness.DataPoint) error { +func (c *Client) DatasetGet(ctx context.Context, dataSourceID string, startTime, endTime time.Time) (*fitness.Dataset, error) { + datasetID := fmt.Sprintf("%d-%d", startTime.UnixNano(), endTime.UnixNano()) + + res, err := c.Service.Users.DataSources.Datasets.Get(userID, dataSourceID, datasetID).Context(ctx).Do() + if err != nil { + return nil, fmt.Errorf("DataSources.Datasets.Get(%q, %q) = %v", dataSourceID, datasetID, err) + } + return res, nil +} + +func (c *Client) DatasetPatch(ctx context.Context, dataSourceID string, points []*fitness.DataPoint) error { startTimeNanos, endTimeNanos := int64(-1), int64(-1) for _, p := range points { if startTimeNanos == -1 || startTimeNanos > p.StartTimeNanos { @@ -140,14 +164,36 @@ func (c *Client) DataSetPatch(ctx context.Context, dataSourceID string, points [ _, err := c.Service.Users.DataSources.Datasets.Patch(userID, dataSourceID, datasetID, dataset).Context(ctx).Do() if err != nil { - log.Errorf(ctx, "c.Service.Users.DataSources.Datasets.Patch() = %v", err) + log.Errorf(ctx, "DataSources.Datasets.Patch(%q, %q) = %v", dataSourceID, datasetID, err) return err } return nil } +func (c *Client) SetDistance(ctx context.Context, meters float64, startOfDay time.Time) error { + return c.updateIncremental(ctx, + &fitness.DataSource{ + Application: Application(ctx), + DataType: &fitness.DataType{ + Field: []*fitness.DataTypeField{ + &fitness.DataTypeField{ + Name: "distance", + Format: "floatPoint", + }, + }, + Name: dataTypeNameDistance, + }, + Name: "Distance covered", + Type: "raw", + }, + &fitness.Value{ + FpVal: meters, + }, + startOfDay) +} + func (c *Client) SetSteps(ctx context.Context, totalSteps int, startOfDay time.Time) error { - return c.updateCumulative(ctx, + return c.updateIncremental(ctx, &fitness.DataSource{ Application: Application(ctx), DataType: &fitness.DataType{ @@ -169,7 +215,7 @@ func (c *Client) SetSteps(ctx context.Context, totalSteps int, startOfDay time.T } func (c *Client) SetCalories(ctx context.Context, totalCalories float64, startOfDay time.Time) error { - return c.updateCumulative(ctx, + return c.updateIncremental(ctx, &fitness.DataSource{ Application: Application(ctx), DataType: &fitness.DataType{ @@ -190,7 +236,81 @@ func (c *Client) SetCalories(ctx context.Context, totalCalories float64, startOf startOfDay) } -func (c *Client) updateCumulative(ctx context.Context, dataSource *fitness.DataSource, rawValue *fitness.Value, startOfDay time.Time) error { +type Activity struct { + Start time.Time + End time.Time + Type string +} + +func (a Activity) String() string { + return fmt.Sprintf("%s-%s %q", a.Start.Format("15:04:05"), a.End.Format("15:04:05"), a.Type) +} + +func (c *Client) SetActivities(ctx context.Context, activities []Activity, startOfDay time.Time) error { + if len(activities) == 0 { + log.Debugf(ctx, "SetActivities(): len(activities) == 0") + return nil + } + + dataStreamID, err := c.DataSourceCreate(ctx, &fitness.DataSource{ + Application: Application(ctx), + DataType: &fitness.DataType{ + Field: []*fitness.DataTypeField{ + &fitness.DataTypeField{ + Name: "activity", + Format: "integer", + }, + }, + Name: dataTypeNameActivitySegment, + }, + Type: "raw", + }) + if err != nil { + return err + } + + endOfDay := startOfDay.Add(24 * time.Hour).Add(-1 * time.Nanosecond) + + dataset, err := c.DatasetGet(ctx, dataStreamID, startOfDay, endOfDay) + if err != nil { + return err + } + + 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 { @@ -211,36 +331,39 @@ func (c *Client) updateCumulative(ctx context.Context, dataSource *fitness.DataS dataSource.DataStreamId = dataSourceID endOfDay := startOfDay.Add(24 * time.Hour).Add(-1 * time.Nanosecond) - currValue, startTime, err := c.readCumulative(ctx, dataSource, startOfDay, endOfDay) + 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 rawValue.IntVal == currValue.IntVal { + if storedValue.IntVal > rawValue.IntVal { + log.Warningf(ctx, "stored value (%d) is larger than new value (%d)", storedValue.IntVal, rawValue.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 + if rawValue.IntVal == storedValue.IntVal { + return nil } + diffValue.IntVal = rawValue.IntVal - storedValue.IntVal } else { // if dataSource.DataType.Field[0].Format == "floatPoint" - if rawValue.FpVal == currValue.FpVal { + if storedValue.FpVal > rawValue.FpVal { + log.Warningf(ctx, "stored value (%g) is larger than new value (%g)", storedValue.FpVal, rawValue.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 + 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, "adding cumulative data point: %v-%v %+v", startTime, endTime, diffValue) + log.Debugf(ctx, "add cumulative data %s until %v: %+v", dataSource.DataStreamId, endTime, diffValue) - return c.DataSetPatch(ctx, dataSource.DataStreamId, []*fitness.DataPoint{ + return c.DatasetPatch(ctx, dataSource.DataStreamId, []*fitness.DataPoint{ &fitness.DataPoint{ DataTypeName: dataSource.DataType.Name, StartTimeNanos: startTime.UnixNano(), @@ -250,22 +373,20 @@ func (c *Client) updateCumulative(ctx context.Context, dataSource *fitness.DataS }) } -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() +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 { - 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 { + 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 res.Point { + for _, p := range dataset.Point { switch f := dataSource.DataType.Field[0].Format; f { case "integer": sum.IntVal += p.Value[0].IntVal @@ -305,21 +426,18 @@ func (res heartRateDurations) find(min, max int) (*heartRateDuration, bool) { } func (c *Client) heartRate(ctx context.Context, dataSource *fitness.DataSource, startTime, endTime time.Time) (heartRateDurations, 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() + dataset, err := c.DatasetGet(ctx, dataSource.DataStreamId, startTime, endTime) 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 { + if len(dataset.Point) == 0 { return nil, startTime, nil } var results heartRateDurations maxEndTime := startTime - for _, p := range res.Point { + 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)) @@ -343,7 +461,7 @@ func (c *Client) heartRate(ctx context.Context, dataSource *fitness.DataSource, return results, maxEndTime, nil } -func (c *Client) SetHeartRate(ctx context.Context, totalDurations []fitbit.HeartRateZone, startOfDay time.Time) error { +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{ @@ -444,5 +562,5 @@ func (c *Client) SetHeartRate(ctx context.Context, totalDurations []fitbit.Heart if len(dataPoints) == 0 { return nil } - return c.DataSetPatch(ctx, dataSource.DataStreamId, dataPoints) + return c.DatasetPatch(ctx, dataSource.DataStreamId, dataPoints) }