├── .gitignore ├── LICENSE ├── README.md ├── examples ├── dump.py └── dump2sqlite.py ├── fitbit ├── __init__.py └── client.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /build 3 | /dist 4 | /*.egg-info 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Wade Simmons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This client provides a simple way to access your data on www.fitbit.com. 4 | I love my fitbit and I want to be able to use the raw data to make my own graphs. 5 | Currently, this client uses the endpoints used by the flash graphs. 6 | Once the official API is announced, this client will be updated to use it. 7 | 8 | Right now, you need to log in to the site with your username / password, and then grab some information from the cookie. 9 | The cookie will look like: 10 | 11 | Cookie: sid=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX; uid=12345; uis=XX%3D%3D; 12 | 13 | Create a `fitbit.Client` with this data, plus the userId (which you can find at the end of your profile url) 14 | 15 | # Example 16 | 17 | import fitbit 18 | 19 | client = fitbit.Client(user_id="XXX", sid="XXX", uid="XXX", uis="XXX") 20 | 21 | # example data 22 | data = client.intraday_steps(datetime.date(2010, 2, 21)) 23 | 24 | # data will be a list of tuples. example: 25 | # [ 26 | # (datetime.datetime(2010, 2, 21, 0, 0), 0), 27 | # (datetime.datetime(2010, 2, 21, 0, 5), 40), 28 | # .... 29 | # (datetime.datetime(2010, 2, 21, 23, 55), 64), 30 | # ] 31 | 32 | # The timestamp is the beginning of the 5 minute range the value is for 33 | 34 | # Other API calls: 35 | data = client.intraday_calories_burned(datetime.date(2010, 2, 21)) 36 | data = client.intraday_active_score(datetime.date(2010, 2, 21)) 37 | 38 | # Sleep data is a little different: 39 | data = client.intraday_sleep(datetime.date(2010, 2, 21)) 40 | 41 | # data will be a similar list of tuples, but spaced one minute apart 42 | # [ 43 | # (datetime.datetime(2010, 2, 20, 23, 59), 2), 44 | # (datetime.datetime(2010, 2, 21, 0, 0), 1), 45 | # (datetime.datetime(2010, 2, 21, 0, 1), 1), 46 | # .... 47 | # (datetime.datetime(2010, 2, 21, 8, 34), 1), 48 | # ] 49 | 50 | # The different values for sleep are: 51 | # 0: no sleep data 52 | # 1: asleep 53 | # 2: awake 54 | # 3: very awake 55 | 56 | There is also an example dump script provided: `examples/dump.py`. This script can be set up as a cron job to dump data nightly. 57 | -------------------------------------------------------------------------------- /examples/dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This is an example script to dump the fitbit data for the previous day. 4 | This can be set up in a cronjob to dump data daily. 5 | 6 | Create a config file at ~/.fitbit.conf with the following: 7 | 8 | [fitbit] 9 | user_id: 12XXX 10 | sid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 11 | uid: 123456 12 | uis: XXX%3D 13 | dump_dir: ~/Dropbox/fitbit 14 | """ 15 | import time 16 | import os 17 | import ConfigParser 18 | 19 | import fitbit 20 | 21 | CONFIG = ConfigParser.ConfigParser() 22 | CONFIG.read(["fitbit.conf", os.path.expanduser("~/.fitbit.conf")]) 23 | 24 | DUMP_DIR=os.path.expanduser(CONFIG.get('fitbit', 'dump_dir')) 25 | 26 | def client(): 27 | return fitbit.Client(CONFIG.get('fitbit', 'user_id'), CONFIG.get('fitbit', 'sid'), CONFIG.get('fitbit', 'uid'), CONFIG.get('fitbit', 'uis')) 28 | 29 | def dump_to_str(data): 30 | return "\n".join(["%s,%s" % (str(ts), v) for ts, v in data]) 31 | 32 | def dump_to_file(data_type, date, data): 33 | directory = "%s/%s" % (DUMP_DIR, data_type) 34 | if not os.path.isdir(directory): 35 | os.makedirs(directory) 36 | with open("%s/%s.csv" % (directory, str(date)), "w") as f: 37 | f.write(dump_to_str(data)) 38 | 39 | def dump_day(date): 40 | c = client() 41 | 42 | dump_to_file("steps", date, c.intraday_steps(date)) 43 | time.sleep(5) 44 | dump_to_file("calories", date, c.intraday_calories_burned(date)) 45 | time.sleep(5) 46 | dump_to_file("active_score", date, c.intraday_active_score(date)) 47 | time.sleep(5) 48 | dump_to_file("sleep", date, c.intraday_sleep(date)) 49 | time.sleep(5) 50 | 51 | if __name__ == '__main__': 52 | #import logging 53 | #logging.basicConfig(level=logging.DEBUG) 54 | import datetime 55 | dump_day((datetime.datetime.now().date() - datetime.timedelta(days=1))) 56 | -------------------------------------------------------------------------------- /examples/dump2sqlite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | This is an example script to dump the fitbit data for the previous day into a sqlite database. 4 | This can be set up in a cronjob to dump data daily. 5 | 6 | Create a config file at ~/.fitbit.conf with the following: 7 | 8 | [fitbit] 9 | user_id: 12XXX 10 | sid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 11 | uid: 123456 12 | uis: XXX%3D 13 | dump_dir: ~/Dropbox/fitbit 14 | db_file: ~/data/nameofdbfile.sqlite 15 | 16 | The database has a table for each of steps, calories, active_score, and sleep. There is also a table with extension _daily for each that contains accumulated data per day. 17 | 18 | The timestamp in the table is a unix timestamp. Tables are set up so that the script can be run repeatedly for the same day. Newer data replaces older data for the same timestamp. This is so data can be caught up if the fitbit does not sync every day. 19 | """ 20 | 21 | from time import mktime, sleep 22 | from datetime import datetime, timedelta 23 | from os import path 24 | import ConfigParser 25 | import sqlite3 26 | 27 | import fitbit 28 | 29 | CONFIG = ConfigParser.ConfigParser() 30 | CONFIG.read(["fitbit.conf", path.expanduser("~/.fitbit.conf")]) 31 | 32 | DB_FILE = path.expanduser(CONFIG.get('fitbit', 'db_file')) 33 | 34 | def client(): 35 | return fitbit.Client(CONFIG.get('fitbit', 'user_id'), CONFIG.get('fitbit', 'sid'), CONFIG.get('fitbit', 'uid'), CONFIG.get('fitbit', 'uis')) 36 | 37 | def create_table(table, db): 38 | db.execute("create table %s (datetime integer PRIMARY KEY ON CONFLICT REPLACE, %s integer)" % (table, table)) 39 | db.execute("create table %s_daily (date integer PRIMARY KEY ON CONFLICT REPLACE, %s integer)" % (table, table)) 40 | 41 | """ Connects to the DB, creates it if it doesn't exist. Returns the connection. 42 | """ 43 | def connect_db(filename): 44 | if path.isfile(filename): 45 | return sqlite3.connect(filename) 46 | else: 47 | db = sqlite3.connect(filename) 48 | create_table("steps", db) 49 | create_table("calories", db) 50 | create_table("active_score", db) 51 | create_table("sleep", db) 52 | return db 53 | 54 | def dump_to_db(db, data_type, date, data): 55 | insertString = "insert into %s values (?, ?)" % data_type 56 | sum = 0 57 | for row in data: 58 | db.execute(insertString, (mktime(row[0].timetuple()), row[1])) 59 | sum += row[1] 60 | db.execute("insert into %s_daily values (?, ?)" % data_type, (mktime(date.timetuple()), sum)) 61 | db.commit() 62 | 63 | def dump_day(db, date): 64 | c = client() 65 | 66 | dump_to_db(db, "steps", date, c.intraday_steps(date)) 67 | sleep(1) 68 | dump_to_db(db, "calories", date, c.intraday_calories_burned(date)) 69 | sleep(1) 70 | dump_to_db(db, "active_score", date, c.intraday_active_score(date)) 71 | sleep(1) 72 | dump_to_db(db, "sleep", date, c.intraday_sleep(date)) 73 | sleep(1) 74 | 75 | if __name__ == '__main__': 76 | db = connect_db(DB_FILE) 77 | 78 | #oneday = timedelta(days=1) 79 | #day = datetime(2009, 10, 18).date() 80 | #while day < datetime.now().date(): 81 | # print day 82 | # dump_day(db, day) 83 | # day += oneday 84 | 85 | dump_day(db, (datetime.now().date() - timedelta(days=1))) 86 | 87 | db.close() 88 | -------------------------------------------------------------------------------- /fitbit/__init__.py: -------------------------------------------------------------------------------- 1 | from fitbit.client import Client 2 | 3 | __all__ = ["Client"] -------------------------------------------------------------------------------- /fitbit/client.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import datetime 3 | import urllib, urllib2 4 | import logging 5 | 6 | _log = logging.getLogger("fitbit") 7 | 8 | class Client(object): 9 | """A simple API client for the www.fitbit.com website. 10 | see README for more details 11 | """ 12 | 13 | def __init__(self, user_id, sid, uid, uis, url_base="http://www.fitbit.com"): 14 | self.user_id = user_id 15 | self.sid = sid 16 | self.uid = uid 17 | self.uis = uis 18 | self.url_base = url_base 19 | self._request_cookie = "sid=%s; uid=%s; uis=%s" % (sid, uid, uis) 20 | 21 | def intraday_calories_burned(self, date): 22 | """Retrieve the calories burned every 5 minutes 23 | the format is: [(datetime.datetime, calories_burned), ...] 24 | """ 25 | return self._graphdata_intraday_request("intradayCaloriesBurned", date) 26 | 27 | def intraday_active_score(self, date): 28 | """Retrieve the active score for every 5 minutes 29 | the format is: [(datetime.datetime, active_score), ...] 30 | """ 31 | return self._graphdata_intraday_request("intradayActiveScore", date) 32 | 33 | def intraday_steps(self, date): 34 | """Retrieve the steps for every 5 minutes 35 | the format is: [(datetime.datetime, steps), ...] 36 | """ 37 | return self._graphdata_intraday_request("intradaySteps", date) 38 | 39 | def intraday_sleep(self, date, sleep_id=None): 40 | """Retrieve the sleep status for every 1 minute interval 41 | the format is: [(datetime.datetime, sleep_value), ...] 42 | The statuses are: 43 | 0: no sleep data 44 | 1: asleep 45 | 2: awake 46 | 3: very awake 47 | For days with multiple sleeps, you need to provide the sleep_id 48 | or you will just get the first sleep of the day 49 | """ 50 | return self._graphdata_intraday_sleep_request("intradaySleep", date, sleep_id=sleep_id) 51 | 52 | def _request(self, path, parameters): 53 | # Throw out parameters where the value is not None 54 | parameters = dict([(k,v) for k,v in parameters.items() if v]) 55 | 56 | query_str = urllib.urlencode(parameters) 57 | 58 | request = urllib2.Request("%s%s?%s" % (self.url_base, path, query_str), headers={"Cookie": self._request_cookie}) 59 | _log.debug("requesting: %s", request.get_full_url()) 60 | 61 | data = None 62 | try: 63 | response = urllib2.urlopen(request) 64 | data = response.read() 65 | response.close() 66 | except urllib2.HTTPError as httperror: 67 | data = httperror.read() 68 | httperror.close() 69 | 70 | #_log.debug("response: %s", data) 71 | 72 | return ET.fromstring(data.strip()) 73 | 74 | def _graphdata_intraday_xml_request(self, graph_type, date, data_version=2108, **kwargs): 75 | params = dict( 76 | userId=self.user_id, 77 | type=graph_type, 78 | version="amchart", 79 | dataVersion=data_version, 80 | chart_Type="column2d", 81 | period="1d", 82 | dateTo=str(date) 83 | ) 84 | 85 | if kwargs: 86 | params.update(kwargs) 87 | 88 | return self._request("/graph/getGraphData", params) 89 | 90 | def _graphdata_intraday_request(self, graph_type, date): 91 | # This method used for the standard case for most intraday calls (data for each 5 minute range) 92 | xml = self._graphdata_intraday_xml_request(graph_type, date) 93 | 94 | base_time = datetime.datetime.combine(date, datetime.time()) 95 | timestamps = [base_time + datetime.timedelta(minutes=m) for m in xrange(0, 288*5, 5)] 96 | values = [int(float(v.text)) for v in xml.findall("data/chart/graphs/graph/value")] 97 | return zip(timestamps, values) 98 | 99 | def _graphdata_intraday_sleep_request(self, graph_type, date, sleep_id=None): 100 | # Sleep data comes back a little differently 101 | xml = self._graphdata_intraday_xml_request(graph_type, date, data_version=2112, arg=sleep_id) 102 | 103 | 104 | elements = xml.findall("data/chart/graphs/graph/value") 105 | timestamps = [datetime.datetime.strptime(e.attrib['description'].split(' ')[-1], "%I:%M%p") for e in elements] 106 | 107 | # TODO: better way to figure this out? 108 | # Check if the timestamp cross two different days 109 | last_stamp = None 110 | datetimes = [] 111 | base_date = date 112 | for timestamp in timestamps: 113 | if last_stamp and last_stamp > timestamp: 114 | base_date -= datetime.timedelta(days=1) 115 | last_stamp = timestamp 116 | 117 | last_stamp = None 118 | for timestamp in timestamps: 119 | if last_stamp and last_stamp > timestamp: 120 | base_date += datetime.timedelta(days=1) 121 | datetimes.append(datetime.datetime.combine(base_date, timestamp.time())) 122 | last_stamp = timestamp 123 | 124 | values = [int(float(v.text)) for v in xml.findall("data/chart/graphs/graph/value")] 125 | return zip(datetimes, values) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup(name="fitbit", 5 | version="0.1", 6 | description="Library for grabbing data from www.fitbit.com", 7 | author="Wade Simmons", 8 | author_email="wade@wades.im", 9 | url="http://github.com/wadey/python-fitbit", 10 | packages = find_packages(), 11 | license = "MIT License", 12 | keywords="fitbit", 13 | zip_safe = True) 14 | --------------------------------------------------------------------------------