DEV Community

Cover image for Working with Google Fit API using Go package "fitness"
Anastasia Penkova
Anastasia Penkova

Posted on

Working with Google Fit API using Go package "fitness"

Google Fit API can be a bit confusing in the beginning and during learning its API I faced many problems and lack of information. In this article I want to share my solutions and give you some examples how to get data from Google Fit.

First step: Enable API

Explanation of enabling API in Google Cloud is pretty long, so I suggest you to read an official article from Google: Enable Google Fit API

Second step: Create a project

Create a project that uses enabled Google Fit API. Also it is better to read it: https://cloud.google.com/resource-manager/docs/creating-managing-projects

Sorry for referencing to many other sources at first, but it gets easier later.

Third step: Create oAuth ClientID

You can find a button Create Credentials in Credentials of your new project. Since we will work with API from our localhost, in Authorised redirect URIs we add a URI: http://127.0.0.1. After that ClientID and ClientSecret are created. They will be necessary in next steps.

Do not share your ClientSecret. It is possible to connect to your API using it.

Creating HTTP service

For simplicity and understanding what data we will get, let's build a small HTTP service using one popular HTTP web framework Gin Gonic.

The structure of our project will be like this:

Project Structur

main.go is our core file where we run all our routes

func main() {
    log.Println("Server started. Press Ctrl-C to stop server")
    controllers.RunAllRoutes()
}
Enter fullscreen mode Exit fullscreen mode

In controllers we have 2 files:

  • handlers.go - here we write our handlers that send requests and get response in JSON format.
  • routes.go - function that runs all our handlers.

In .secrets there should be 2 files with format .dat. That's where we need our copied ClientID and ClientSecret. Here we just paste each of them to these 2 files.

In models we have 2 files:

  • models.go - we store all our structs
  • consts.go - we store all necessary constant variables and maps

In google-api I used a Go Fitness package example where I always referenced to fitness.go, copy-pasted a file debug.go and some parts of main.go

  • init.go consists of some parts of main.go from the example.

The most important part of the file is an authorisation of a client using Google token:

// authClient returns HTTP client using Google token from cache or web
func authClient(ctx context.Context, config *oauth2.Config) *http.Client {
    cacheFile := tokenCacheFile(config)
    token, err := tokenFromFile(cacheFile)
    if err != nil {
        token = tokenFromWeb(ctx, config)
        saveToken(cacheFile, token)
    } else {
        log.Printf("Using cached token %#v from %q", token, cacheFile)
    }

    return config.Client(ctx, token)
}
Enter fullscreen mode Exit fullscreen mode

Either Google token is stored in .secret as a file, or a user authorises themselves via browser and then the token is saved in .secret:

Google Authorisation Page

  • get.go consists of functions that sends requests to get specific data (s.t. hydration, steps, weight, etc.) in different ways that I will explain later in this article.

  • parse.go parses different datasets depending on data type

Getting data from Google Fit API

For more detailed information about this API you can look at the official Google Fit API documentation.

Go package fitness is a very helpful tool that we will use this time.

Let's look at 2 ways to get data.

Data aggregation

If data can be continuously recorded, Google Fit can aggregate the data by calculating average values or summarising the data.

As an example, we want to get aggregated weight data.
In google-api/get.go let's write a function GetDatasetBody. It actually can get not only weight data, but height data as well.

func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Variables startTime and endTime are necessary for a request to specify period of records. I still did not find information for how long this period can be. In my cases I will request data within last 30 days. dataType for this function can be weight or height.

At first we need to specify scopes. Since weight data is a body data type and we just want to read data, not to write anything, scope will be https://www.googleapis.com/auth/fitness.body.read. You can find out more about scopes here: https://developers.google.com/fit/datatypes and https://developers.google.com/fit/datatypes/aggregate.

Then by running the function authClient it authorises a user using a cached Google token or if it does not exist, user should consent the right to read body data.

func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    flag.Parse()
    config, err := returnConfig([]string{
        fitness.FitnessBodyReadScope,
    })
    if err != nil {
        log.Println("returnConfig error", err.Error())
        return nil, err
    }

    if *debug {
        ctx = context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
            Transport: &logTransport{http.DefaultTransport},
        })
    }

    // returning HTTP client using user's token and configs of the application
    client := authClient(ctx, config)
    svc, err := fitness.NewService(ctx, option.WithHTTPClient(client))
    if err != nil {
        log.Println("NewService error", err.Error())
        return nil, err
    }
   ...
}
Enter fullscreen mode Exit fullscreen mode

After we create an authorised HTTP client, let's create request for an aggregated dataset.

func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    ...
    // in AggregateRequest we use milliseconds for StartTimeMillis and EndTimeMillis,
    // while in response we get time in nanoseconds
    payload := fitness.AggregateRequest{
        AggregateBy: []*fitness.AggregateBy{
            {DataTypeName: "com.google." + dataType},
        },
        BucketByTime: &fitness.BucketByTime{
            Period: &fitness.BucketByTimePeriod{
                Type:       "day",
                Value:      1,
                TimeZoneId: "GMT",
            },
        },
        StartTimeMillis: TimeToMillis(startTime),
        EndTimeMillis:   TimeToMillis(endTime),
    }

    weightData, err := svc.Users.Dataset.Aggregate("me", &payload).Do()
    if err != nil {
        return nil, errors.New("Unable to query aggregated weight data:" + err.Error())
    }

    return weightData, nil
}
Enter fullscreen mode Exit fullscreen mode

There are some parameters by which you can aggregate data. For example, aggregation by period, it can be "day", "week", "month". In our example we want daily data. StartTime and EndTime has to be formatted to milliseconds. Using only one function from fitness Go package, we simply get an aggregated result.

The result is stored in fitness.AggregateResponse struct. At first sight response is a bit confusing and not quite readable:

Weight Data

There are a lot of questions. Why do we have 3 values? What kind of time is it? Is it nanoseconds? Milliseconds?

Let's look at the official documentation: https://developers.google.com/fit/datatypes/aggregate

Weight documentation

So we can see that these 3 values are average, maximal and minimal values within one day. I made 2 records in Google Fit app for an example: 59kg and 60kg, so there are min and max values. And 59,5kg is an average value of my weight in this day. There may be many values within one day in database of Google Fit, but in aggregated weight dataset we always get three values.

Time is stored in nanoseconds. We format it using function NanosToTime in init.go.

To get a nice data overview, let's parse this struct.

func ParseData(ds *fitness.AggregateResponse, dataType string) []models.Measurement {
    var data []models.Measurement

    for _, res := range ds.Bucket {
        for _, ds := range res.Dataset {
            for _, p := range ds.Point {
                var row models.Measurement
                row.AvValue = p.Value[0].FpVal
                row.MinValue = p.Value[1].FpVal
                row.MaxValue = p.Value[2].FpVal
                row.StartTime = NanosToTime(p.StartTimeNanos)
                row.EndTime = NanosToTime(p.EndTimeNanos)
                row.Type = dataType
                data = append(data, row)
            }
        }
    }
    return data
}
Enter fullscreen mode Exit fullscreen mode

After parsing it our data will look like this:

Parsed Weight Data

Of course, we can round values, it's up to you.

Now let us look at another more customised way to get data.

Customised request

For this example we want to get hydration data.

Google Fitness has data sources (https://developers.google.com/fit/rest/v1/data-sources) that describe each source of sensor data. Data sources can be different: application in your phone where you manually insert records, apps or devices that automatically insert records. Data sources are separated not only by a source, but also by a data type. So if we need data from some specific data source that is synchronised with Google Fitness, we can get it without problems.

func NotAggregatedDatasets(svc *fitness.Service, minTime, maxTime time.Time, dataType string) ([]*fitness.Dataset, error) {
    ds, err := svc.Users.DataSources.List("me").DataTypeName("com.google." + dataType).Do()
    if err != nil {
        log.Println("Unable to retrieve user's data sources:", err)
        return nil, err
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

Here dataType is "hydration". More about data types in Google Fit nutrition see here: https://developers.google.com/fit/datatypes/nutrition

In each data source you can find list of datasets.

func NotAggregatedDatasets(svc *fitness.Service, minTime, maxTime time.Time, dataType string) ([]*fitness.Dataset, error) {
    ds, err := svc.Users.DataSources.List("me").DataTypeName("com.google." + dataType).Do()
    if err != nil {
        log.Println("Unable to retrieve user's data sources:", err)
        return nil, err
    }
    if len(ds.DataSource) == 0 {
        log.Println("You have no data sources to explore.")
        return nil, err
    }

    var dataset []*fitness.Dataset

    for _, d := range ds.DataSource {
        setID := fmt.Sprintf("%v-%v", minTime.UnixNano(), maxTime.UnixNano())
        data, err := svc.Users.DataSources.Datasets.Get("me", d.DataStreamId, setID).Do()
        if err != nil {
            log.Println("Unable to retrieve dataset:", err.Error())
            return nil, err
        }
        dataset = append(dataset, data)
    }

    return dataset, nil

}
Enter fullscreen mode Exit fullscreen mode

Resulted data will look like this:

Hydration data

Again data is not quite readable. Let's parse it.

func ParseHydration(datasets []*fitness.Dataset) []models.HydrationStruct {
    var data []models.HydrationStruct

    for _, ds := range datasets {
        var value float64
        for _, p := range ds.Point {
            for _, v := range p.Value {
                valueString := fmt.Sprintf("%.3f", v.FpVal)
                value, _ = strconv.ParseFloat(valueString, 64)
            }
            var row models.HydrationStruct
            row.StartTime = NanosToTime(p.StartTimeNanos)
            row.EndTime = NanosToTime(p.EndTimeNanos)
            // liters to milliliters
            row.Amount = int(value * 1000)
            data = append(data, row)
        }
    }
    return data
}
Enter fullscreen mode Exit fullscreen mode

Then data will look much better!

Parsed hydration data

Conclusion

Yay! Now you can request Google Fit to get health data. Here I showed you only two examples. For more info, see my repository where you will find how to get data, s.t. nutrition, height, steps, heart points, heart rate, active minutes, burned calories and activity segments.


I will try my best to comment code to understand it better and hope you enjoyed reading my first article ever!

Next article

Stay tuned for the second part where I will explain how to write data into tricky Google Fit, so your application will be fully synchronised!

If you have more questions or suggestions, I will be very happy to hear from you!

Top comments (2)

Collapse
 
athieticinsight profile image
Athletic Insight

Awesome stuff!! Google bough Fitbit, right?

Parker
Owner | Athletic Insight

Collapse
 
saqib_akmal1 profile image
𝙼 𝚂𝚊𝚚𝚒𝚋 𝙰𝚔𝚖𝚊𝚕 • Edited

Fitnesp is a blogging website for health and fitnesp. must visit fitnesp