Getting your weight from Google Fit with C#

I think Google Fit is a wonderful platform. It connects multiple devices and apps to give you insight into your fitness. I connected a Withing Smart Body Analyzer scale to the system. What I'm missing is a simple min / max weight per day graph, something like this:

Google Fit Graph
Overview of the graph.

This blog describes how to get the weight data out of Google Fit into your C# application. You can use your own chats solution to plot a graph like this.

1. Google Fit NuGet Package

Google was kind enough to create a C# wrapper around the Google Fit API and provided a NuGet package for us:

  • Install-Package Google.Apis.Fitness.v1 -Version 1.49.0.2143
  • dotnet add package Google.Apis.Fitness.v1 --version 1.49.0.2143
  • <PackageReference Include="Google.Apis.Fitness.v1" Version="1.49.0.2143" />

2. Scope, credentials + service

Next you'll need to get your credentials ready. How you do this is outside the scope of this tutorial as it depends on what platform you use. The credential needs to be created with scope FitnessService.Scope.FitnessBodyRead. The FitnessService needs a UserCredential. I've implemented the Google MVC Authentication flow that will give met a result that provides a credential:

var credential = {...} 
var service = new FitnessService(new BaseClientService.Initializer()
{
    ApplicationName = Assembly.GetExecutingAssembly().GetName().Name,
    HttpClientInitializer = credential
});

You can read more on Offline Google Authentication for MVC.Net here.

3. Base class for querying

Google Fit works with data sets that can be queried using a DataSourceId and a DataSourceType. The query mechanism will seem somewhat weird at first as it uses a myriad of objects. To make things easier, I've created an abstract FitnessQuery class. It facilitates the creation and execution of the queries on the data sets.

To make handling of time easier I've created the GoogleTime class - it is appended at the end of the article.

using Google.Apis.Fitness.v1;
using Google.Apis.Fitness.v1.Data;
using System;

/// <summary>
/// Helper class that hides the complexity of the fitness API.
/// </summary>
public abstract class FitnessQuery
{
    private FitnessService _service;
    private string _dataSourceId;
    private string _dataType;

    /// <summary>
    /// Initializes a new instance of the <see cref="FitnessQuery"/> class.
    /// </summary>
    /// <param name="service">The service.</param>
    /// <param name="dataSourceId">The data source identifier.</param>
    /// <param name="dataType">Type of the data.</param>
    public FitnessQuery(FitnessService service, string dataSourceId, string dataType)
    {
        _service = service;
        _dataSourceId = dataSourceId;
        _dataType = dataType;
    }

    /// <summary>
    /// Creates the request.
    /// </summary>
    /// <param name="start">The start.</param>
    /// <param name="end">The end.</param>
    /// <returns>The request.</returns>
    protected virtual AggregateRequest CreateRequest(DateTime start, DateTime end, TimeSpan? bucketDuration = null)
    {
        var bucketTimeSpan = bucketDuration.GetValueOrDefault(TimeSpan.FromDays(1));

        return new AggregateRequest
        {
            AggregateBy = new AggregateBy[] {
                new AggregateBy
                {
                    DataSourceId = _dataSourceId,
                    DataTypeName = _dataType
                }
            },

            BucketByTime = new BucketByTime
            {
                DurationMillis = (long)bucketTimeSpan.TotalMilliseconds
            },

            StartTimeMillis = GoogleTime.FromDateTime(start).TotalMilliseconds,
            EndTimeMillis = GoogleTime.FromDateTime(end).TotalMilliseconds

        };
    }

    /// <summary>
    /// Executes the request.
    /// </summary>
    /// <param name="request">The request.</param>
    /// <param name="userId">The user identifier.</param>
    /// <returns>
    /// The response.
    /// </returns>
    protected virtual AggregateResponse ExecuteRequest(AggregateRequest request, string userId = "me")
    {
        var agg = _service.Users.Dataset.Aggregate(request, userId);
        return agg.Execute();
    }
}

4. Querying weight

Next I created the WeightQuery class. It will query data source derived:com.google.weight:com.google.android.gms:merge_weight using data type com.google.body..

using Google.Apis.Fitness.v1;
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Queries the weight data source of Google Fit.
/// </summary>
public class WeightQuery : FitnessQuery
{
    private const string DataSource = "derived:com.google.weight:com.google.android.gms:merge_weight";
    private const string DataType = "com.google.weight.summary";

    /// <summary>
    /// Initializes a new instance of the <see cref="WeightQuery"/> class.
    /// </summary>
    /// <param name="service">The service.</param>
    public WeightQuery(FitnessService service) :
        base(service, DataSource, DataType)
    {
    }

    /// <summary>
    /// Queries the weight.
    /// </summary>
    /// <param name="start">The start.</param>
    /// <param name="end">The end.</param>
    /// <returns>A list with weight data points.</returns>
    public IList<WeightDataPoint> QueryWeight(DateTime start, DateTime end)
    {
        var request = CreateRequest(start, end);
        var response = ExecuteRequest(request);

        return response
            .Bucket
            .SelectMany(b => b.Dataset)
            .Where(d => d.Point != null)
            .SelectMany(d => d.Point)
            .Where(p => p.Value != null)
            .SelectMany(p =>
            {
                return p.Value.Select(v =>
                    new WeightDataPoint
                    {
                        Weight = v.FpVal.GetValueOrDefault(),
                        Stamp = GoogleTime.FromNanoseconds(p.StartTimeNanos).ToDateTime()
                    });
            })
            .ToList();
    }

    /// <summary>
    /// Queries the weight.
    /// </summary>
    /// <param name="start">The start.</param>
    /// <param name="end">The end.</param>
    /// <returns>A list with weight data points.</returns>
    public IList<WeightDataPoint> QueryWeightPerDay(DateTime start, DateTime end)
    {
        return QueryWeight(start, end)
            .OrderBy(w => w.Stamp)
            .GroupBy(w => w.Stamp.Date)
            .Select(g => new WeightDataPoint
            {
                Stamp = g.Key,
                MaxWeight = g.Max(w => w.Weight),
                MinWeight = g.Min(w => w.Weight),
            })
            .ToList();
    }
}

I use LINQ to query the content of the response and convert it to a WeightDataPoint object. The QueryWeight(...) method returns a list with all the data points. The QueryWeightPerDay(...) will also return a list, but it will return a unique data point per day with a max and a min weight. Only the data part of the stamp will be used in this method.

using System;

/// <summary>
/// Weight data point.
/// </summary>
public class WeightDataPoint
{
    /// <summary>
    /// Gets or sets the weight.
    /// </summary>
    /// <value>
    /// The weight.
    /// </value>
    public double? Weight { get; set; }

    /// <summary>
    /// Gets or sets the stamp.
    /// </summary>
    /// <value>
    /// The stamp.
    /// </value>
    public DateTime Stamp { get; set; }

    /// <summary>
    /// Gets or sets the maximum weight.
    /// </summary>
    /// <value>
    /// The maximum weight.
    /// </value>
    public double? MaxWeight { get; set; }

    /// <summary>
    /// Gets or sets the minimum weight.
    /// </summary>
    /// <value>
    /// The minimum weight.
    /// </value>
    public double? MinWeight { get; set; }
}

5. Mixing it together in a data table

I'm using Google Charts to plot the graph. It uses a data table - basically jagged array. Again, I'll use LINQ to create the array.

if (this.Credential == null)
{
    return RedirectToAction("Index", "Home");
}

var service = new FitnessService(new BaseClientService.Initializer()
{
    ApplicationName = Assembly.GetExecutingAssembly().GetName().Name,
    HttpClientInitializer = Credential
});

var query = new WeightQuery(service);
var list = query.QueryWeightPerDay(DateTime.Now.AddDays(-60), DateTime.Now);

var dataTable = list
    .Select(w => new object[]
    {
        w.Stamp,
        w.MinWeight,
        w.MaxWeight
    })
    .ToArray();

This dataTable can be serialized to JSON and used to create a Google Chart.

Handling time

The Google API uses milliseconds and nanoseconds. In C# I like to work with simple DateTime structures, that why I created the GoogleTime class that helps with the conversion between the two formats:

using System;

/// <summary>
/// Helps converting between miliseconds since 1970-1-1 and C# <c>DateTime</c> object.
/// </summary>
public class GoogleTime
{
    private static readonly DateTime zero = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    /// <summary>
    /// Gets the total milliseconds.
    /// </summary>
    /// <value>
    /// The total milliseconds.
    /// </value>
    public long TotalMilliseconds { get; private set; }

    /// <summary>
    /// Prevents a default instance of the <see cref="GoogleTime"/> class from being created.
    /// </summary>
    private GoogleTime()
    {
    }

    /// <summary>
    /// Create a time object from the given date time.
    /// </summary>
    /// <param name="dt">The date time.</param>
    /// <returns>The time object.</returns>
    public static GoogleTime FromDateTime(DateTime dt)
    {
        if (dt < zero)
        {
            throw new Exception("Invalid Google datetime.");
        }

        return new GoogleTime
        {
            TotalMilliseconds = (long)(dt - zero).TotalMilliseconds,
        };
    }

    /// <summary>
    /// Creates a time object from the given nanoseconds.
    /// </summary>
    /// <param name="nanoseconds">The ns.</param>
    /// <returns>The time object.</returns>
    public static GoogleTime FromNanoseconds(long? nanoseconds)
    {
        if (nanoseconds < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(nanoseconds), "Must be greater than 0.");
        }

        return new GoogleTime
        {
            TotalMilliseconds = (long)(nanoseconds.GetValueOrDefault(0) / 1000000)
        };
    }

    /// <summary>
    /// Gets the current time.
    /// </summary>
    /// <value>
    /// The current time.
    /// </value>
    public static GoogleTime Now
    {
        get { return FromDateTime(DateTime.Now); }
    }

    /// <summary>
    /// Adds the specified time span.
    /// </summary>
    /// <param name="timeSpan">The time span.</param>
    /// <returns></returns>
    public GoogleTime Add(TimeSpan timeSpan)
    {
        return new GoogleTime
        {
            TotalMilliseconds = this.TotalMilliseconds + (long)timeSpan.TotalMilliseconds
        };
    }

    /// <summary>
    /// Converts this instance into a <c>DateTime</c> object.
    /// </summary>
    /// <returns>The date time.</returns>
    public DateTime ToDateTime()
    {
        return zero.AddMilliseconds(this.TotalMilliseconds);
    }
}

Final thoughts

Querying Google Fit using data sets, sources and types might seem difficult - but it is doable using NuGet and the documentation. Hopefully they'll make it easier.

  1. Maciek says:

    Good job men , thanks

  2. camlelia says:

    Thanks

  3. camlelia says:

    Please share the github link if possible.

    1. Kees C. Bakker says:

      This was a small weekend project. I did not deploy it to GitHub. The Google Fit API is pretty straight forward. As the implementation is pretty case-specific I thought it would not be good for GitHub users. If you put the code on GitHub I’ll append it to the article.

  4. Dam Ske says:

    Hi,

    Nice post. Big thanks.
    Just only one question : How did you find that you should do that to manage it. because I am beginner in dev and when i read google Doc about google FIt. Nothing make me think how to code as you did.

    1. Kees C. Bakker says:

      I’ve been programming for some year and I first encoutered Google when I was exploring its Cloud Vision API. So I got to know a little bit about their NuGet packages. After that it took some experimenting and a loooooot of Googling to find this out. That’s why I share these things online, so that other ppl don’t have to Google so much.

      1. Dam Ske says:

        Nice thanks you.
        I am looking a way to retrieve activity. I found some stuff but it give me only 3 exercice while in my google fit account I find many waling activity. then I am not sure my way is working, do you have an already existing class to retrieve activity ?

  5. Neel says:

    I duplicated same code in visual studio and updated required values. But still code is not working for me. I am getting null value for credential. Can you please help me ?

    1. Kees C. Bakker says:

      Yeah I can. Is it possible for you to share your project on GitHub. This gives me the ability to look into your code. If your repo is private, you could DM me on Twitter and send me the link to your repo. Would that work?

      1. Neel says:

        Very thanks for your reply.

        Now, I am able to get value for credential. I removed [Authorize] attribute from code and changed if-else condition so it can call “AuthorizeWithGoogle” method which has solved my issue.

        But now I have to go further with Google Fit API and I definitely need your guidance and help.

        Now, I want to get all data sources and data points for all data types of Google Fit. Do you have any sample example or some links I can refer/use ? I need data in real-time. For now history data will work but later I have to get data in real-time.

        Thanks
        Neel

        1. Kees C. Bakker says:

          I’ve found that getting data from Google is near real time. It had a delay of a view minutes. I don’t think it can be used in real time. (not sure though)

          1. Neel says:

            Now, I want to get all data sources and data points for all data types of Google Fit. Do you have any sample example or some links I can refer/use ?

expand_less