Fitbit: koppeling met de API

Ingediend door Dirk Hornstra op 07-may-2018 20:46

Vorig jaar is de fabrikant van de Jawbone, ook een activity-tracker, failliet gegaan. Ik neem aan dat dan de site offline gaat en (als daar een soort dashboard voor jouw data was) je dat dus niet meer kunt bekijken. Bye-bye historie. Zelf heb ik een klassieke Fitbit (5 lampjes, wekker in de ochtend), de simpele versie, dus je kunt er ongeveer een week mee doen voordat je hem weer op moet laden. Ook heb ik aan mijn andere pols een Polar M400. Ook die is zuinig qua verbruik. Notificeert je ook als je een lange tijd stil zit. En heeft ingebouwde GPS, wat erg makkelijk is om later je route nog even terug te kijken. Anders moet je een losse GPS mee nemen of Endomondo op je telefoon aanzetten.

Eerst de Fitbit onder handen nemen Op dev.fitbit.com zie je in het kort de stappen. Je moet een app aanmaken en via OAuth 2.0 kun je de acties uitvoeren. Bij de post over de koppeling van Drupal/LinkedIn kwam ook al OAuth 2.0 naar voren, dit zal vergelijkbaar zijn, alleen werk ik het nu in C# code uit, omdat ik heb ook met mijn "dashing"-dashboard wil verwerken: het project om te testen of dashing ook op basis van .NET werkt.

Eerst de app aanmaken. Hiervoor ga je naar https://dev.fitbit.com/login en log je in. Ik kom dan weer terug op de overzichtspagina (??) ga dan terug naar de inlogpagina en krijg te zien dat er een verificatie-mail gestuurd wordt. Je moet dan op die knop klikken, je krijgt de melding dat de mail verzonden is. Ik ontvang inderdaad een mail, voer de verificatie uit en het is klaar. Hierna klik ik op het tabblad "Register an App". Als naam "Dirks Data Dump" gegeven. Overige gegevens invullen, bij de callback-url https://mijndomein/fitbit/callback ingevuld. Gekozen voor type "server". Alleen read-rechten, we willen alleen lezen. Na het opslaan krijg ik een overzicht van de Client ID, Client Secret en de URL's. Nu wordt het tijd om het .NET-project op te zetten.Welke functies je moet/kunt implementeren, dat kun je vinden op https://dev.fitbit.com/build/reference/web-api/

In Visual Studio 2017 begin ik met een "leeg project". Ik wil zelf een project opzetten (bij andere projecten gebruik ik bestaande code) zodat ik nu eens zelf kan ondervinden hoe het werkt. Goede basis voor mijn certificering, want nu begrijp je wat er allemaal gebeurt. Dit project valt deels samen met mijn vorige post, EntityFramework en mySQL in .NET, omdat ik de data wil opslaan in een mySQL-database. De structuur wordt als volgt;

map API
Ik maak een map "Api" aan waar ik mijn FitbitApiController (voor het koppelen met de app van Fitbit) en FitbitCronController aanmaak. Die cron-controller roep ik regelmatig aan (via cron) om mijn data op te vragen en op te slaan. Het gaat hier eerst om de activiteiten en mijn slaap-gegevens.

map App_Start
In de map App_Start staan de (als het goed is) welbekende Bundleconfig.cs, Filterconfig.cs, Routeconfig.cs en WepApiConfig.cs. Deze worden in de global.asax ingesteld en zorgen voor de routering binnen je webapplicatie.

Assets
Een mapje "Assets" waar ik mijn Badges en Profielfoto opsla, stylesheets, logo.

Constants (wordt verwijderd)
In eerste instantie heb ik hier een map "Constants" waarin de vaste waardes (locaties) configureer voor het opslaan van de bestanden, hier kom ik nog op terug.

Controllers
In de map Controllers komen de classes die zorgen dat je aanroep een schermpje laat zien. Ik maak hier een FitbitController, in eerste instantie om wat met de data te testen, later kan deze class wel weg, ook hier kom ik op terug. 

Helpers (wordt verwijderd)
In deze map komen 2 classes, de AssetHelper, die zorgt dat als je een URL meegeeft waar de badge staat, deze lokaal ergens in je Assets opgeslagen wordt. De andere class is de DatabaseHelper. Dit is een klein stukje code wat de juiste databaseconnectie teruggeeft, een mySQL connectie of een SQL Server Compact verbinding.

Models (wordt verwijderd)
De data die je uit de Fitbit-API terugkrijgt is JSON-data. Als je zelf een class maakt die overeen komt met de structuur van de JSON die je ontvangt kun je simpelweg met een JsonConvert.DeserializeObject<JFitbitAtivities>(new StreamReader(....)) in je code vervolgens de data opslaan. 


 

    public class JFitBitActivities : IDisposable
    {
        public JActivityActivityData[] activities;
        public JActivityGoalData goals;
        public JActivitySummaryData summary;
        public void Dispose() { }
    }

    public class JActivityActivityData
    {
        public string activityId;
        public int activityParentId;
        public int calories;
        public string description;
        public double distance;
        public int duration;
        public bool hasStartTime;
        public bool isFavorite;
        public int logId;
        public string name;
        public string startTime;
        public int steps;
    }

    public class JActivityGoalData
    {
        public int activeMinutes;
        public int caloriesOut;
        public double distance;
        public int floors;
        public int steps;
    }

    public class JActivitySummaryData
    {
        public int activityCalories;
        public int caloriesBMR;
        public int caloriesOut;
        public JActivitySummaryDistanceData[] distances;
        public double elevation;
        public int fairlyActiveMinutes;
        public int floors;
        public int lightlyActiveMinutes;
        public int marginalCalories;
        public int sedentaryMinutes;
        public int steps;
        public int veryActiveMinutes;
        public decimal TotalDistance { get {
                var total = distances.Where(rec => rec.activity.Equals("total")).FirstOrDefault();
                if (total != null)
                {
                    return total.distance;
                }
                return 0;
            } }
        public int FairlyAndVeryActiveMinutes
        {
            get { return fairlyActiveMinutes + veryActiveMinutes; }
        }
    }

    public class JActivitySummaryDistanceData    
    {
        public string activity;
        public decimal distance;
    }

Views
In de submap Fitbit heb ik een aantal Razor cshtml-bestanden staan waarmee ik bijvoorbeeld mijn badges toon, gegevens van activiteiten. Maar eigenlijk meer om in het begin te kunnen controleren wat er aan data binnenkomt.

Hiermee heb ik zitten testen en uiteindelijk heb ik een oplossing waarmee ik content ben. Het token wat je van de Fitbit-API krijgt is ongeveer een dag geldig. Daarna moet je het refresh-token meegeven, krijg je een nieuw token en refresh-token en kun je weer door. Werkt allemaal erg fijn.

Maar wat nog mooier zou zijn is dat ik zelf een soort dashboard zou hebben. Dat heeft Fitbit nu zelf ook. Toegegeven, Fitbit heeft meer data. Je kunt daar de pieken per uur van de dag zien. Maar dan moet je project wel van type "personal app type" zijn of iets dergelijks. Ik filter zelf al zaken weg, per dag het aantal stappen, calorieën en kilometers en van het slaap-log de begintijd, eindtijd, efficiëntie, rusteloze en wakkere minuten, aantal keren wakker, aantal keren rusteloos en de totale duur in milliseconden dat ik geslapen heb is prima. Dat dashboard heb ik al, dashing.net

Ik ga deze twee projecten samenvoegen, dat zorgt ook dat bepaalde onderdelen verdwijnen (eigenlijk verplaatst worden).
Het dashing.net project bestaat uit een aantal projecten. Ik voeg de zaken toe in mijn "Health-project". Ook heb je nog een dashing.net.common, dashing.net.streaming en dashing.net.jobs-project. De fitbit-controller verwijder ik omdat deze niet meer nodig is, alles gaat nu via de API of het dashboard.

In mijn Healt-project verwijs ik dus naar deze 3 projecten. Dan kom je op het punt dat je de "jobs" moet gaan inrichten. Op het scherm wil ik het aantal kilometers, het aantal stappen van de afgelopen 7 dagen tonen. In een teller die tot 100 gaat mijn slaap-efficiëntie van het laatste slaap-record. In een teller die tot 100 gaat mijn weekdoel (op hoeveel procent van mijn weekdoel ben ik inmiddels, die begint bij maandag, begin van de week). En een blokje met het aantal stappen van de laatst opgeslagen dag vergeleken met dezelfde dag vorig jaar, mocht die er niet zijn, dan ten opzichte van een week ervoor (toepasselijk in het "karma"-blokje geplaatst).

Het opvragen van de data wordt uitgevoerd in het dashing.net.jobs-project. Maar die kun je niet laten verwijzen naar het Health-project, omdat deze al verwijst naar het jobs-project (we hebben die taken nodig om data op het scherm te tonen). We verplaatsen het model, de constanten en de helpers daarom naar een eigen Health-Data project. Zo kunnen we dus vanuit het Health-project onze data aanvullen vanuit de Cron-API en de data opvragen in het jobs-project.

Omdat het geen zin heeft continu de data te verversen, heb ik dit ingesteld op 1 minuut (daarom in de blokken ook de melding dat het een minuut kan duren voor de data getoond wordt). Het verversen van de data zelf gebeurt één keer in de vijf minuten, ook die waarde kan denk ik nog wel omhoog. Hierbij nog even de code van de "karma":


 

namespace dashing.net.jobs
{
    [Export(typeof(IJob))]
    public class Karma : IJob
    {

        public int CurrentKarma { get; private set; }
        public int LastKarma { get; private set; }

        public Lazy<Timer> Timer { get; private set; }
        private bool _update = true;
        private DateTime? LastCheck = null;

        public Karma()
        {

            SetLastAndCurrentKarma();
            Timer = new Lazy<Timer>(() => new Timer(SendMessage, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)));
        }

        private bool SetLastAndCurrentKarma()
        {
            bool result = false;
            using (var entities = DatabaseHelper.getEntityModelDatabase())
            {
                var lastLog = entities.FitbitActivities.OrderByDescending(rec => rec.date).First().date;
                DateTime previousYear = lastLog.AddYears(-1);
                DateTime previousWeek = lastLog.AddDays(-7);
                var oldKarma = entities.FitbitActivities.Where(rec => rec.date.Equals(previousYear)).FirstOrDefault();
                if (oldKarma != null)
                {
                    CurrentKarma = oldKarma.steps;
                    result = true;
                }
                else
                {
                    CurrentKarma = entities.FitbitActivities.Where(rec => rec.date >= previousWeek).OrderBy(rec => rec.date).First().steps;
                }
            }
            return result;
        }

        public void SendMessage(object sent)
        {
            bool doCheck = true;
            string moreInfo = "";
            if (LastCheck.HasValue && LastCheck.Value.AddMinutes(-5) < DateTime.Now)
            {
                doCheck = false;
            }
            if (doCheck)
            {
                if (SetLastAndCurrentKarma())
                {
                    moreInfo = "t.o.v. vorig jaar";
                }
                else
                {
                    moreInfo = "t.o.v. vorige week";
                }
                LastKarma = CurrentKarma;

                using (var entities = DatabaseHelper.getEntityModelDatabase())
                {
                    CurrentKarma = entities.FitbitActivities.OrderByDescending(rec => rec.date).First().steps;
                }
            }


            Dashing.SendMessage(new {current = CurrentKarma, last = LastKarma, id = "karma", moreinfo = moreInfo});
        }
    }
}
 

Het heeft allemaal even geduurd, maar ik ben blij met het resultaat. Het vullen van data kan nog even duren, mijn eerste logging vond plaats eind juni 2013, dus ik laat nu om het kwartier de data bijwerken. 4 weken in 1 uur, 52 weken in 13 uur, over ongeveer 52 uur zou het aardig up-to-date moeten zijn.

Mocht je nieuwsgierig zijn geworden, ik heb het project eerst hier neergezet:

https://fitbit.prijs-bewust.nl/