├── .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 | data 6 | -------------------------------------------------------------------------------- /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 all available fitbit data. 4 | This can be set up in a cronjob to dump data daily. 5 | 6 | You can add your passwords to a file ~/.fitbit in the form below and then you don't need to include it on the command line. 7 | 8 | email1:password1 9 | email2:password2 10 | 11 | Run with the following parameters: 12 | $ python dump.py 13 | """ 14 | import datetime 15 | import os 16 | import sys 17 | import time 18 | 19 | sys.path.append(os.getcwd()) 20 | 21 | import fitbit 22 | 23 | EMAIL=sys.argv[1] 24 | 25 | if len(sys.argv) == 3: 26 | fp = open(os.path.expanduser("~/.fitbit"), "r") 27 | PASSWORD = [line.split(":")[1] for line in fp.readlines() if line.split(":")[0] == EMAIL][0].strip() 28 | 29 | DUMP_DIR=sys.argv[2] 30 | else: 31 | PASSWORD = sys.argv[2] 32 | DUMP_DIR = sys.argv[3] 33 | 34 | def dump_to_str(data): 35 | return "\n".join(["%s,%s" % (str(ts), v) for ts, v in data]) 36 | 37 | def dump_to_file(data_type, date, data): 38 | directory = "%s/%i/%s" % (DUMP_DIR, date.year, date) 39 | if not os.path.isdir(directory): 40 | os.makedirs(directory) 41 | with open("%s/%s.csv" % (directory, data_type), "w") as f: 42 | f.write(dump_to_str(data)) 43 | time.sleep(1) 44 | 45 | def previously_dumped(date): 46 | return os.path.isdir("%s/%i/%s" % (DUMP_DIR, date.year, date)) 47 | 48 | def dump_day(c, date): 49 | steps = c.intraday_steps(date) 50 | # Assume that if no steps were recorded then there is no data 51 | if sum([s[1] for s in steps]) == 0: 52 | return False 53 | 54 | dump_to_file("steps", date, steps) 55 | dump_to_file("calories", date, c.intraday_calories_burned(date)) 56 | dump_to_file("active_score", date, c.intraday_active_score(date)) 57 | dump_to_file("sleep", date, c.intraday_sleep(date)) 58 | 59 | return True 60 | 61 | if __name__ == '__main__': 62 | #import logging 63 | #logging.basicConfig(level=logging.DEBUG) 64 | client = fitbit.Client.login(EMAIL, PASSWORD) 65 | 66 | date = datetime.date.today() 67 | 68 | # Look for the most recent sync 69 | while (datetime.date.today() - date).days < 365: 70 | r = dump_day(client, date) 71 | date -= datetime.timedelta(days=1) 72 | if r: 73 | break 74 | 75 | if (datetime.date.today() - date).days > 365: 76 | # No sync in the last year. 77 | sys.exit(1) 78 | 79 | while not previously_dumped(date): 80 | r = dump_day(client, date) 81 | date -= datetime.timedelta(days=1) 82 | if not r: 83 | break 84 | 85 | # Always redump the last dumped day because we may have dumped it before the day was finished. 86 | dump_day(client, date) 87 | -------------------------------------------------------------------------------- /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 | 4 | import urllib 5 | try: 6 | import urllib2 7 | except ImportError: 8 | import urllib.request 9 | import urllib.parse 10 | Request = urllib.request.Request 11 | build_opener = urllib.request.build_opener 12 | HTTPCookieProcessor = urllib.request.HTTPCookieProcessor 13 | urlencode = urllib.parse.urlencode 14 | HTTPError = urllib.HTTPError 15 | else: 16 | Request = urllib2.Request 17 | build_opener = urllib2.build_opener 18 | HTTPCookieProcessor = urllib2.HTTPCookieProcessor 19 | urlencode = urllib.urlencode 20 | HTTPError = urllib2.HTTPError 21 | import logging 22 | import re 23 | try: 24 | import cookielib 25 | except ImportError: 26 | import http.cookiejar as cookielib 27 | 28 | _log = logging.getLogger("fitbit") 29 | 30 | class Client(object): 31 | """A simple API client for the www.fitbit.com website. 32 | see README for more details 33 | """ 34 | 35 | def __init__(self, user_id, opener, url_base="http://www.fitbit.com"): 36 | self.user_id = user_id 37 | self.opener = opener 38 | self.url_base = url_base 39 | 40 | def intraday_calories_burned(self, date): 41 | """Retrieve the calories burned every 5 minutes 42 | the format is: [(datetime.datetime, calories_burned), ...] 43 | """ 44 | return self._graphdata_intraday_request("intradayCaloriesBurned", date) 45 | 46 | def intraday_active_score(self, date): 47 | """Retrieve the active score for every 5 minutes 48 | the format is: [(datetime.datetime, active_score), ...] 49 | """ 50 | return self._graphdata_intraday_request("intradayActiveScore", date) 51 | 52 | def intraday_steps(self, date): 53 | """Retrieve the steps for every 5 minutes 54 | the format is: [(datetime.datetime, steps), ...] 55 | """ 56 | return self._graphdata_intraday_request("intradaySteps", date) 57 | 58 | def intraday_sleep(self, date, sleep_id=None): 59 | """Retrieve the sleep status for every 1 minute interval 60 | the format is: [(datetime.datetime, sleep_value), ...] 61 | The statuses are: 62 | 0: no sleep data 63 | 1: asleep 64 | 2: awake 65 | 3: very awake 66 | For days with multiple sleeps, you need to provide the sleep_id 67 | or you will just get the first sleep of the day 68 | """ 69 | return self._graphdata_intraday_sleep_request("intradaySleep", date, sleep_id=sleep_id) 70 | 71 | def _request(self, path, parameters): 72 | # Throw out parameters where the value is not None 73 | parameters = dict([(k,v) for k,v in parameters.items() if v]) 74 | 75 | query_str = urlencode(parameters) 76 | 77 | request = Request("%s%s?%s" % (self.url_base, path, query_str)) 78 | _log.debug("requesting: %s", request.get_full_url()) 79 | 80 | data = None 81 | try: 82 | response = self.opener.open(request) 83 | data = response.read() 84 | response.close() 85 | except HTTPError as httperror: 86 | data = httperror.read() 87 | httperror.close() 88 | 89 | #_log.debug("response: %s", data) 90 | 91 | return ET.fromstring(data.strip().replace("…", "...")) 92 | 93 | def _graphdata_intraday_xml_request(self, graph_type, date, data_version=2108, **kwargs): 94 | params = dict( 95 | userId=self.user_id, 96 | type=graph_type, 97 | version="amchart", 98 | dataVersion=data_version, 99 | chart_Type="column2d", 100 | period="1d", 101 | dateTo=str(date) 102 | ) 103 | 104 | if kwargs: 105 | params.update(kwargs) 106 | 107 | return self._request("/graph/getGraphData", params) 108 | 109 | def _graphdata_intraday_request(self, graph_type, date): 110 | # This method used for the standard case for most intraday calls (data for each 5 minute range) 111 | xml = self._graphdata_intraday_xml_request(graph_type, date) 112 | 113 | base_time = datetime.datetime.combine(date, datetime.time()) 114 | timestamps = [base_time + datetime.timedelta(minutes=m) for m in range(0, 288*5, 5)] 115 | values = [int(float(v.text)) for v in xml.findall("data/chart/graphs/graph/value")] 116 | return zip(timestamps, values) 117 | 118 | def _graphdata_intraday_sleep_request(self, graph_type, date, sleep_id=None): 119 | # Sleep data comes back a little differently 120 | xml = self._graphdata_intraday_xml_request(graph_type, date, data_version=2112, arg=sleep_id) 121 | 122 | 123 | elements = xml.findall("data/chart/graphs/graph/value") 124 | try: 125 | timestamps = [datetime.datetime.strptime(e.attrib['description'].split(' ')[-1], "%I:%M%p") for e in elements] 126 | except ValueError: 127 | timestamps = [datetime.datetime.strptime(e.attrib['description'].split(' ')[-1], "%H:%M") for e in elements] 128 | 129 | # TODO: better way to figure this out? 130 | # Check if the timestamp cross two different days 131 | last_stamp = None 132 | datetimes = [] 133 | base_date = date 134 | for timestamp in timestamps: 135 | if last_stamp and last_stamp > timestamp: 136 | base_date -= datetime.timedelta(days=1) 137 | last_stamp = timestamp 138 | 139 | last_stamp = None 140 | for timestamp in timestamps: 141 | if last_stamp and last_stamp > timestamp: 142 | base_date += datetime.timedelta(days=1) 143 | datetimes.append(datetime.datetime.combine(base_date, timestamp.time())) 144 | last_stamp = timestamp 145 | 146 | values = [int(float(v.text)) for v in xml.findall("data/chart/graphs/graph/value")] 147 | return zip(datetimes, values) 148 | 149 | @staticmethod 150 | def login(email, password, base_url="https://www.fitbit.com"): 151 | cj = cookielib.CookieJar() 152 | opener = build_opener(HTTPCookieProcessor(cj)) 153 | opener.addheaders = [("User-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36")] 154 | 155 | # fitbit.com wierdness - as of 2014-06-20 the /login page gives a 500: Internal Server Error 156 | # if there's no cookie 157 | # Workaround: open the https://www.fitbit.com/ page first to get a cookie 158 | opener.open(base_url) 159 | 160 | # Get the login page so we can load the magic values 161 | login_page = opener.open(base_url + "/login").read().decode("utf8") 162 | 163 | source_page = re.search(r"""name="_sourcePage".*?value="([^"]+)["]""", login_page).group(1) 164 | fp = re.search(r"""name="__fp".*?value="([^"]+)["]""", login_page).group(1) 165 | 166 | data = urlencode({ 167 | "email": email, "password": password, 168 | "_sourcePage": source_page, "__fp": fp, 169 | "login": "Log In", "includeWorkflow": "false", 170 | "redirect": "", "rememberMe": "true" 171 | }).encode("utf8") 172 | 173 | logged_in = opener.open(base_url + "/login", data) 174 | 175 | if logged_in.geturl() == "http://www.fitbit.com/" or logged_in.geturl() == "https://www.fitbit.com/" \ 176 | or logged_in.geturl() == "https://www.fitbit.com:443/": 177 | page = logged_in.read().decode("utf8") 178 | 179 | match = re.search(r"""userId=([a-zA-Z0-9]+)""", page) 180 | if match is None: 181 | match = re.search(r"""/user/([a-zA-Z0-9]+)" """, page) 182 | user_id = match.group(1) 183 | 184 | return Client(user_id, opener, base_url) 185 | else: 186 | raise ValueError("Incorrect username or password.") 187 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------