|
| 1 | +# SPDX-FileCopyrightText: 2023 DJDevon3 |
| 2 | +# SPDX-License-Identifier: MIT |
| 3 | +# Coded for Circuit Python 8.2 |
| 4 | + |
| 5 | +import os |
| 6 | +import board |
| 7 | +import time |
| 8 | +import microcontroller |
| 9 | +import ssl |
| 10 | +import wifi |
| 11 | +import socketpool |
| 12 | +import adafruit_requests |
| 13 | + |
| 14 | +# Initialize WiFi Pool (There can be only 1 pool & top of script) |
| 15 | +pool = socketpool.SocketPool(wifi.radio) |
| 16 | + |
| 17 | +# STREAMER WARNING: private data will be viewable while debug True |
| 18 | +debug = False # Set True for full debug view |
| 19 | + |
| 20 | +# Can use to confirm first instance of NVM is correct refresh token |
| 21 | +top_nvm = microcontroller.nvm[0:64].decode() |
| 22 | +if debug: |
| 23 | + print(f"Top NVM: {top_nvm}") # NVM before settings.toml loaded |
| 24 | + |
| 25 | +# --- Fitbit Developer Account & oAuth App Required: --- |
| 26 | +# Required: Google Login (Fitbit owned by Google) & Fitbit Device |
| 27 | +# Step 1: Create a personal app here: https://dev.fitbit.com |
| 28 | +# Step 2: Use their Tutorial to get the Token and first Refresh Token |
| 29 | +# Fitbit's Tutorial Step 4 is as far as you need to go. |
| 30 | +# https://dev.fitbit.com/build/reference/web-api/troubleshooting-guide/oauth2-tutorial/ |
| 31 | + |
| 32 | +# Ensure these are in settings.toml |
| 33 | +# Fitbit_ClientID = "YourAppClientID" |
| 34 | +# Fitbit_Token = "Long 256 character string (SHA-256)" |
| 35 | +# Fitbit_First_Refresh_Token = "64 character string" |
| 36 | +# Fitbit_UserID = "UserID authorizing the ClientID" |
| 37 | + |
| 38 | +Fitbit_ClientID = os.getenv("Fitbit_ClientID") |
| 39 | +Fitbit_Token = os.getenv("Fitbit_Token") |
| 40 | +Fitbit_First_Refresh_Token = os.getenv( |
| 41 | + "Fitbit_First_Refresh_Token" |
| 42 | +) # overides nvm first run only |
| 43 | +Fitbit_UserID = os.getenv("Fitbit_UserID") |
| 44 | + |
| 45 | +wifi_ssid = os.getenv("CIRCUITPY_WIFI_SSID") |
| 46 | +wifi_pw = os.getenv("CIRCUITPY_WIFI_PASSWORD") |
| 47 | + |
| 48 | +# Time between API refreshes |
| 49 | +# 300 = 5 mins, 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour |
| 50 | +sleep_time = 900 |
| 51 | + |
| 52 | + |
| 53 | +# Converts seconds in minutes/hours/days |
| 54 | +def time_calc(input_time): |
| 55 | + if input_time < 60: |
| 56 | + sleep_int = input_time |
| 57 | + time_output = f"{sleep_int:.0f} seconds" |
| 58 | + elif 60 <= input_time < 3600: |
| 59 | + sleep_int = input_time / 60 |
| 60 | + time_output = f"{sleep_int:.0f} minutes" |
| 61 | + elif 3600 <= input_time < 86400: |
| 62 | + sleep_int = input_time / 60 / 60 |
| 63 | + time_output = f"{sleep_int:.1f} hours" |
| 64 | + else: |
| 65 | + sleep_int = input_time / 60 / 60 / 24 |
| 66 | + time_output = f"{sleep_int:.1f} days" |
| 67 | + return time_output |
| 68 | + |
| 69 | + |
| 70 | +# Authenticates Client ID & SHA-256 Token to POST |
| 71 | +fitbit_oauth_header = {"Content-Type": "application/x-www-form-urlencoded"} |
| 72 | +fitbit_oauth_token = "https://api.fitbit.com/oauth2/token" |
| 73 | + |
| 74 | +# Connect to Wi-Fi |
| 75 | +print("\n===============================") |
| 76 | +print("Connecting to WiFi...") |
| 77 | +requests = adafruit_requests.Session(pool, ssl.create_default_context()) |
| 78 | +while not wifi.radio.ipv4_address: |
| 79 | + try: |
| 80 | + wifi.radio.connect(wifi_ssid, wifi_pw) |
| 81 | + except ConnectionError as e: |
| 82 | + print("Connection Error:", e) |
| 83 | + print("Retrying in 10 seconds") |
| 84 | + time.sleep(10) |
| 85 | +print("Connected!\n") |
| 86 | + |
| 87 | +# First run uses settings.toml token |
| 88 | +Refresh_Token = Fitbit_First_Refresh_Token |
| 89 | + |
| 90 | +if debug: |
| 91 | + print(f"Top NVM Again (just to make sure): {top_nvm}") |
| 92 | + print(f"Settings.toml Initial Refresh Token: {Fitbit_First_Refresh_Token}") |
| 93 | + |
| 94 | +while True: |
| 95 | + # Use Settings.toml refresh token on first run |
| 96 | + if top_nvm != Fitbit_First_Refresh_Token: |
| 97 | + Refresh_Token = microcontroller.nvm[0:64].decode() |
| 98 | + if debug: |
| 99 | + # NVM 64 should match Current Refresh Token |
| 100 | + print(f"NVM 64: {microcontroller.nvm[0:64].decode()}") |
| 101 | + print(f"Current Refresh_Token: {Refresh_Token}") |
| 102 | + else: |
| 103 | + if debug: |
| 104 | + # First run use settings.toml refresh token instead |
| 105 | + print(f"Initial_Refresh_Token: {Refresh_Token}") |
| 106 | + |
| 107 | + try: |
| 108 | + if debug: |
| 109 | + print("\n-----Token Refresh POST Attempt -------") |
| 110 | + fitbit_oauth_refresh_token = ( |
| 111 | + "&grant_type=refresh_token" |
| 112 | + + "&client_id=" |
| 113 | + + str(Fitbit_ClientID) |
| 114 | + + "&refresh_token=" |
| 115 | + + str(Refresh_Token) |
| 116 | + ) |
| 117 | + |
| 118 | + # ----------------------------- POST FOR REFRESH TOKEN ----------------------- |
| 119 | + if debug: |
| 120 | + print( |
| 121 | + f"FULL REFRESH TOKEN POST:{fitbit_oauth_token}{fitbit_oauth_refresh_token}" |
| 122 | + ) |
| 123 | + print(f"Current Refresh Token: {Refresh_Token}") |
| 124 | + # TOKEN REFRESH POST |
| 125 | + fitbit_oauth_refresh_POST = requests.post( |
| 126 | + url=fitbit_oauth_token, |
| 127 | + data=fitbit_oauth_refresh_token, |
| 128 | + headers=fitbit_oauth_header, |
| 129 | + ) |
| 130 | + try: |
| 131 | + fitbit_refresh_oauth_json = fitbit_oauth_refresh_POST.json() |
| 132 | + |
| 133 | + fitbit_new_token = fitbit_refresh_oauth_json["access_token"] |
| 134 | + if debug: |
| 135 | + print("Your Private SHA-256 Token: ", fitbit_new_token) |
| 136 | + fitbit_access_token = fitbit_new_token # NEW FULL TOKEN |
| 137 | + |
| 138 | + # If current token valid will respond |
| 139 | + fitbit_new_refesh_token = fitbit_refresh_oauth_json["refresh_token"] |
| 140 | + Refresh_Token = fitbit_new_refesh_token |
| 141 | + fitbit_token_expiration = fitbit_refresh_oauth_json["expires_in"] |
| 142 | + fitbit_scope = fitbit_refresh_oauth_json["scope"] |
| 143 | + fitbit_token_type = fitbit_refresh_oauth_json["token_type"] |
| 144 | + fitbit_user_id = fitbit_refresh_oauth_json["user_id"] |
| 145 | + if debug: |
| 146 | + print("Next Refresh Token: ", Refresh_Token) |
| 147 | + |
| 148 | + # Store Next Token into NVM |
| 149 | + try: |
| 150 | + nvmtoken = b"" + fitbit_new_refesh_token |
| 151 | + microcontroller.nvm[0:64] = nvmtoken |
| 152 | + if debug: |
| 153 | + print(f"Next Token for NVM: {nvmtoken.decode()}") |
| 154 | + print(f"Next token written to NVM Successfully!") |
| 155 | + except OSError as e: |
| 156 | + print("OS Error:", e) |
| 157 | + continue |
| 158 | + |
| 159 | + if debug: |
| 160 | + # Extraneous token data for debugging |
| 161 | + print("Token Expires in: ", time_calc(fitbit_token_expiration)) |
| 162 | + print("Scope: ", fitbit_scope) |
| 163 | + print("Token Type: ", fitbit_token_type) |
| 164 | + print("UserID: ", fitbit_user_id) |
| 165 | + |
| 166 | + except KeyError as e: |
| 167 | + print("Key Error:", e) |
| 168 | + print("Expired token, invalid permission, or (key:value) pair error.") |
| 169 | + time.sleep(300) |
| 170 | + continue |
| 171 | + |
| 172 | + # ----------------------------- GET DATA ------------------------------------- |
| 173 | + # POST should respond with current & next refresh token we can GET for data |
| 174 | + # 64-bit Refresh tokens will "keep alive" SHA-256 token indefinitely |
| 175 | + # Fitbit main SHA-256 token expires in 8 hours unless refreshed! |
| 176 | + # ---------------------------------------------------------------------------- |
| 177 | + detail_level = "1min" # Supported: 1sec | 1min | 5min | 15min |
| 178 | + requested_date = "today" # Date format yyyy-MM-dd or today |
| 179 | + fitbit_header = { |
| 180 | + "Authorization": "Bearer " + fitbit_access_token + "", |
| 181 | + "Client-Id": "" + Fitbit_ClientID + "", |
| 182 | + } |
| 183 | + # Heart Intraday Scope |
| 184 | + FITBIT_SOURCE = ( |
| 185 | + "https://api.fitbit.com/1/user/" |
| 186 | + + Fitbit_UserID |
| 187 | + + "/activities/heart/date/today" |
| 188 | + + "/1d/" |
| 189 | + + detail_level |
| 190 | + + ".json" |
| 191 | + ) |
| 192 | + |
| 193 | + print("\nAttempting to GET FITBIT Stats!") |
| 194 | + print("===============================") |
| 195 | + fitbit_get_response = requests.get(url=FITBIT_SOURCE, headers=fitbit_header) |
| 196 | + try: |
| 197 | + fitbit_json = fitbit_get_response.json() |
| 198 | + except ConnectionError as e: |
| 199 | + print("Connection Error:", e) |
| 200 | + print("Retrying in 10 seconds") |
| 201 | + |
| 202 | + if debug: |
| 203 | + print(f"Full API GET URL: {FITBIT_SOURCE}") |
| 204 | + print(f"Header: {fitbit_header}") |
| 205 | + # print(f"JSON Full Response: {fitbit_json}") |
| 206 | + # print(f"Intraday Full Response: {fitbit_json["activities-heart-intraday"]["dataset"]}") |
| 207 | + |
| 208 | + try: |
| 209 | + # Fitbit's sync to your mobile device & server every 15 minutes in chunks. |
| 210 | + # Pointless to poll their API faster than 15 minute intervals. |
| 211 | + activities_heart_value = fitbit_json["activities-heart-intraday"]["dataset"] |
| 212 | + response_length = len(activities_heart_value) |
| 213 | + if response_length >= 15: |
| 214 | + activities_timestamp = fitbit_json["activities-heart"][0]["dateTime"] |
| 215 | + print(f"Fitbit Date: {activities_timestamp}") |
| 216 | + activities_latest_heart_time = fitbit_json["activities-heart-intraday"][ |
| 217 | + "dataset" |
| 218 | + ][response_length - 1]["time"] |
| 219 | + print(f"Fitbit Time: {activities_latest_heart_time[0:-3]}") |
| 220 | + print(f"Today's Logged Pulses : {response_length}") |
| 221 | + |
| 222 | + # Each 1min heart rate is a 60 second average |
| 223 | + activities_latest_heart_value0 = fitbit_json[ |
| 224 | + "activities-heart-intraday" |
| 225 | + ]["dataset"][response_length - 1]["value"] |
| 226 | + activities_latest_heart_value1 = fitbit_json[ |
| 227 | + "activities-heart-intraday" |
| 228 | + ]["dataset"][response_length - 2]["value"] |
| 229 | + activities_latest_heart_value2 = fitbit_json[ |
| 230 | + "activities-heart-intraday" |
| 231 | + ]["dataset"][response_length - 3]["value"] |
| 232 | + activities_latest_heart_value3 = fitbit_json[ |
| 233 | + "activities-heart-intraday" |
| 234 | + ]["dataset"][response_length - 4]["value"] |
| 235 | + activities_latest_heart_value4 = fitbit_json[ |
| 236 | + "activities-heart-intraday" |
| 237 | + ]["dataset"][response_length - 5]["value"] |
| 238 | + activities_latest_heart_value5 = fitbit_json[ |
| 239 | + "activities-heart-intraday" |
| 240 | + ]["dataset"][response_length - 6]["value"] |
| 241 | + activities_latest_heart_value6 = fitbit_json[ |
| 242 | + "activities-heart-intraday" |
| 243 | + ]["dataset"][response_length - 7]["value"] |
| 244 | + activities_latest_heart_value7 = fitbit_json[ |
| 245 | + "activities-heart-intraday" |
| 246 | + ]["dataset"][response_length - 8]["value"] |
| 247 | + activities_latest_heart_value8 = fitbit_json[ |
| 248 | + "activities-heart-intraday" |
| 249 | + ]["dataset"][response_length - 9]["value"] |
| 250 | + activities_latest_heart_value9 = fitbit_json[ |
| 251 | + "activities-heart-intraday" |
| 252 | + ]["dataset"][response_length - 10]["value"] |
| 253 | + activities_latest_heart_value10 = fitbit_json[ |
| 254 | + "activities-heart-intraday" |
| 255 | + ]["dataset"][response_length - 11]["value"] |
| 256 | + activities_latest_heart_value11 = fitbit_json[ |
| 257 | + "activities-heart-intraday" |
| 258 | + ]["dataset"][response_length - 12]["value"] |
| 259 | + activities_latest_heart_value12 = fitbit_json[ |
| 260 | + "activities-heart-intraday" |
| 261 | + ]["dataset"][response_length - 13]["value"] |
| 262 | + activities_latest_heart_value13 = fitbit_json[ |
| 263 | + "activities-heart-intraday" |
| 264 | + ]["dataset"][response_length - 14]["value"] |
| 265 | + activities_latest_heart_value14 = fitbit_json[ |
| 266 | + "activities-heart-intraday" |
| 267 | + ]["dataset"][response_length - 15]["value"] |
| 268 | + print( |
| 269 | + f"Latest 15 Minute Averages: {activities_latest_heart_value14},{activities_latest_heart_value13},{activities_latest_heart_value12},{activities_latest_heart_value11},{activities_latest_heart_value10},{activities_latest_heart_value9},{activities_latest_heart_value8},{activities_latest_heart_value7},{activities_latest_heart_value6},{activities_latest_heart_value5},{activities_latest_heart_value4},{activities_latest_heart_value3},{activities_latest_heart_value2},{activities_latest_heart_value1},{activities_latest_heart_value0}" |
| 270 | + ) |
| 271 | + else: |
| 272 | + print(f"Waiting for latest 15 values sync...") |
| 273 | + print(f"Not enough values for today to display yet.") |
| 274 | + print(f"No display from midnight to 00:15") |
| 275 | + |
| 276 | + except KeyError as keyerror: |
| 277 | + print(f"Key Error: {keyerror}") |
| 278 | + print( |
| 279 | + f"Too Many Requests, Expired token, invalid permission, or (key:value) pair error." |
| 280 | + ) |
| 281 | + continue |
| 282 | + |
| 283 | + print("Board Uptime: ", time_calc(time.monotonic())) # Board Up-Time seconds |
| 284 | + print("\nFinished!") |
| 285 | + print("Next Update in: ", time_calc(sleep_time)) |
| 286 | + print("===============================") |
| 287 | + |
| 288 | + except (ValueError, RuntimeError) as e: |
| 289 | + print("Failed to get data, retrying\n", e) |
| 290 | + time.sleep(60) |
| 291 | + continue |
| 292 | + time.sleep(sleep_time) |
0 commit comments