Working with Google Fit API using Go package "fitness"
Tags: go, googlefitness, googlefit
Published: 2022-08-31 15:10:44

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](https://support.google.com/googleapi/answer/6158841?hl=en)

## 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](https://github.com/gin-gonic/gin).

The structure of our project will be like this:

![Project Structur](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lwg7ktyyyamd9ctmkhob.png)

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

```go
func main() {
	log.Println("Server started. Press Ctrl-C to stop server")
	controllers.RunAllRoutes()
}
```
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](https://github.com/googleapis/google-api-go-client/blob/main/examples) 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:

```go
// 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)
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ylqtrsk5w62mhuegn3n8.png)

- `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](https://developers.google.com/fit).

Go package [`fitness`](https://pkg.go.dev/google.golang.org/api/fitness/v1) 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.
```go
func GetDatasetBody(ctx context.Context, startTime, endTime time.Time, dataType string) (*fitness.AggregateResponse, error) {
    ...
}
```

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.

```go
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
	}
   ...
}
```

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

```go
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
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m0fcn4wsvj49sr2vz8bj.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8p6ka7etztppk4xd7bv3.png)

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.
```go
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
}
```

After parsing it our data will look like this:

![Parsed Weight Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50b17mhgpn6unurrcaj5.png)

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.

```go
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
	}

    ...
}
```

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.
```go
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

}
```

Resulted data will look like this:

![Hydration data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/et7pd48hziv7ow390eft.png)

Again data is not quite readable. Let's parse it.
```go
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
}
```

Then data will look much better!

![Parsed hydration data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/70i60lygx0yx3v7xhqwc.png)

## 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.

{% embed https://github.com/bronnika/devto-google-fit %}

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!