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

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»

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.

namespace KeesTalksTech.Examples.GoogleFitApi.Queries
{
    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..

namespace KeesTalksTech.Examples.GoogleFitApi.Queries
{
    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.

namespace KeesTalksTech.Examples.GoogleFitApi.Queries
{
    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:

namespace KeesTalksTech.Examples.GoogleFitApi
{
    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.