Fitbit2Garmin Part 1: Fitbit API

project Apr 07, 2021

It's an imaginative name, right?

I recently got myself a mighty fine Garmin smartwatch and so my awareness of my health has increased (I'm expecting my actual health to increase any day now). The app/site Garmin Connect does a pretty nice job of collating all your data such as steps, sleep, exercises yada yada.... Everything in one easy to read location, so what's the problem? Well, I've got a Fitbit branded smart scales (really this entire thing is my fault for buying anything smart that really shouldn't be) and would really like for my weight data from that to be automatically synced across to my Garmin account. The alternatives are to buy a Garmin smart scale (wasteful) or, ugh, manually update my data every time I weigh myself (never gonna happen).

So where does that leave me? With a plan to build an application that pulls data from my Fitbit account and sends it across to my Garmin account. Fortunately, it's 2021, the world is connected, and of course, of course these big companies have APIs that lets end users access and update their data, right? Let's check out the documentation for Fitbit.

Screenshot of Fitbit documentation for their API
Fitbit looks like they have a pretty open API

So far, so good! So presumably Garmin must be as flexible with their.....

Screenshot of Garmin Documentation for their API
Garmin looks like they have a very not open API

....ok then. Looks like we're going to have to do this the hard way, Garmin.

So what's the plan then? The application is going to grab data from the Fitbit API and then somehow shove it into my Garmin account. It will be written in Python (honestly the best thing to use in this case). We can worry about the Garmin API (or lack there of) later, lets get cracking with the Fitbit API first.

Using the Fitbit API

As mentioned above, Fitbit have a pretty nice set of documentation for their API so it shouldn't be too difficult to get the information we want. To use the API, we need to authenticate and then we'll be free to grab whatever information we want.

Authenticating

Fitbit uses OAuth 2.0 for authentication (like any good API should), and supports a couple of different styles depending on the use case. For this project (essentially a personal application communicating with Fitbit), we'll use the Authorization Code Grant Flow. We'll get an authorization token, and then a refresh token we'll need to periodically use to refresh the authorization token.

Firstly, lets set up the initial framework for the program, just so we can get a sense of what we want. We'll make a main app file that will tie all our logic together, with some nice logging.

import FitbitApi
import logging

# Phase 1:
# Authenticate with Fitbit - get access token and refresh token
# Every 8 hours, refresh token
# Once a day, sync data

if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG, filename='app.log', filemode='a'
                        , format='%(name)s - %(levelname)s - %(message)s')
    logging.info("Starting Fitbit2Garmin!")
    # Call authenticate once when app starts
    FitbitApi.authenticate()
Base outline for app

The logic for interacting with the Fitbit API will be contained in its own class, so let's get authenticating!

We'll use the oauthlib Python library to help with the authentication requests, it certainly wasn't the first one I found on Google. To authenticate with Fitbit, we're going to need to direct the user to an authorization page controlled by Fitbit. On that page, the user will enter their credentials, and then they'll get redirected back to the app with an authorization code that the app can then use to get an access token and a refresh token. But, hmm, hang on a second, I don't really want to build a UI for this thing, really it's just going to headlessly operate on a computer somewhere, so I'm going to cheat a little bit. I'm going to manually authenticate against the Fitbit API to get my authorization token and then use that as a starting parameter for my app. Once it's got the authorization token, it'll be able to self-sufficiently authenticate against the Fitbit API. Is that how you're supposed to do it? No! But I just want my damn smart things to talk to each other, and I'm not going to be selling this app. Added bonus, we can do all of this using the standard requests library and using it will let us see in exact detail how we're building these requests. Hooray for learning!

Authenticating: Getting the Initial Tokens

First off, I need to register my app on https://dev.fitbit.com which will give me the secret data the app will use for directing users (aka me) to authenticate. Then I'm going to have to do some super complex awkward.....

Well then....

So, Fitbit provide a super-handy tutorial page which will do exactly what I need it to (namely, give me an authorization token that my app can then use to get access and refresh tokens). Awesome, they get points for this. I'll use this to get the authorization code, then we'll pass that to the app on startup, which will use it to get the initial authorization and refresh codes.

Let's write the code that's going to take the initial authorization token and get an access and refresh token. This function will only need to be invoked once, when the app is started.

# Get the first access token
def __authenticate(authorization_code):
    global access_token, refresh_token
    logging.debug('Getting access token using authorization code')

    header = __build_basic_authorization_header()

    # Set body values
    body_parameters = {'code': authorization_code, 'grant_type': 'authorization_code', 'client_id': client_id, 'redirect_uri': redirect_uri}

    # Send the request to the Fitbit token URL, the response will contain an access token and refresh token
    response = requests.post(fitbit_token_url, headers=header, data=body_parameters)
    if response.status_code != 200:
        logging.error('Error requesting initial authorization token')
        logging.error(response.text)
        exit(-1)

    # Store these in global variables for access by other methods
    access_token = response.json()['access_token']
    refresh_token = response.json()['refresh_token']
    
# Builds the Basic auth header used for requesting access tokens
def __build_basic_authorization_header():
    # Yes, could be doing this all with an OAuth library, but coding it this way works well as an educational tool
    authorization_string = '%s:%s' % (client_id, client_secret)
    # Python expects a bytes-like object for b64 encoding. Can do that simply by encoding it as ascii
    base64_authorization_string = base64.b64encode(authorization_string.encode('ascii'))
    return {'Authorization': 'Basic %s' % base64_authorization_string.decode('ascii')}
Python code for exchanging an authorization token for an access token

We can see how the authorization code is used, as data in the body of the request. We also build the authorization header in its own function, as that will be reused later when refreshing the token. The code block uses some variables that I haven't shown being defined yet, like the client ID and client secret. I don't want to store those in the code itself, so lets store them in an ini file and use some Python magic to read them

# Read needed values from properties file
def __init_variables():
    global client_id, client_secret

    config = configparser.ConfigParser()
    config.sections()
    config.read('config.ini')
    client_id = config['FITBIT']['client_id']
    client_secret = config['FITBIT']['client_secret']

    __verify_init_variables()

    logging.debug('Client ID and Client Secret read from config file as %s and %s', client_id, client_secret)
    
    
def __verify_init_variables():
    if client_id is None:
        logging.error('Client ID is null, unable to proceed')
        exit(-1)
    if client_secret is None:
        logging.error('Client Secret is null, unable to proceed')
        exit(-1)
Python code for reading variables from an ini file

Ok, there's one more part to the authorization flow before we get to the meat of actually asking the Fitbit API for any data and that is handling the refresh of tokens. Using OAuth2, when you exchange an authorization token for an access token, you're also given a refresh token. That access token is only good for a certain amount of time, and once the time expires it cannot be used anymore. In that case we'll use the refresh token to get a new access token, along with a new refresh token to use when the new access token expires. Fortunately, refresh tokens don't have an expiry time, so we shouldn't need to worry about the refresh token not working when we try to use it (unless it's been revoked). Having built the initial logic for getting the first access token, the logic for refresh is pretty straightforward.

# Renews the access token using the refresh token
def __renew_token():
    global access_token, refresh_token
    logging.debug('Refreshing access token')

    header = __build_basic_authorization_header()

    # Set body values
    body_parameters = {'refresh_token': refresh_token, 'grant_type': 'refresh_token'}

    # Send the request to the Fitbit token URL, the response will contain an access token and refresh token
    response = requests.post(fitbit_token_url, headers=header, data=body_parameters)
    if response.status_code != 200:
        logging.error('Unable to successfully renew token through Fitbit API')
        logging.error(response.text)
        exit(-1)

    access_token = response.json()['access_token']
    refresh_token = response.json()['refresh_token']
Logic for using a refresh token to get a new access token

Done. All that's left for this part is to actually, finally, get the data we want from Fitbit, which in this case is the weight logged for today. Lets make an assumption for now that I'll only want this app to get my weight data for the current date. The Fitbit API makes it easy enough to specific what time period we want the data for, so lets build a small function to handle this request.

# Builds data request and sends to fitbit, returning the response
def __send_data_request_to_fitbit():
    headers = {'Authorization': 'Bearer %s' % access_token}

    # Get weight for today's date by building appropriate URL
    date = datetime.today().strftime('%Y-%m-%d')
    full_url = fitbit_user_api_url + fitbit_user_api_body_weight.replace('[date]', date)
    # todo: add in something that reduces the recorded weight by like......15% or so
    return requests.get(full_url, headers=headers)

Ok then, we've got some code that will take an authorization token passed in as a runtime parameter, exchange that for an access token, then use that access token to get weight data from Fitbit, with some additional logic to renew the token if required. That's all we need to do on the Fitbit side of things, the next steps are to parse the data, shove it into my Garmin account (somehow), and then automate the process so it runs on a regular basis.

That's all coming up in a future post, so stay tuned! For now the source code for what's been done so far is available here.

Tags