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:
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()
}
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 ofmain.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)
}
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
:
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) {
...
}
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
}
...
}
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
}
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:
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
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
}
After parsing it our data will look like this:
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
}
...
}
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
}
Resulted data will look like this:
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
}
Then data will look much better!
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)
Awesome stuff!! Google bough Fitbit, right?
Parker
Owner | Athletic Insight
Fitnesp is a blogging website for health and fitnesp. must visit fitnesp