pydexcom
A simple Python API to interact with Dexcom Share service. Used to get real-time Dexcom CGM sensor data.
Quick-start
- Download the Dexcom G7 / G6 / G5 / G4 mobile app and enable the Share service.
The Dexcom Share service requires setup of at least one follower to enable the share service, but pydexcom
will use your (or the dependent's) credentials, not the follower's or manager's.
- Install the
pydexcom
package.
pip install pydexcom
- Profit.
>>> from pydexcom import Dexcom
>>> dexcom = Dexcom(username="username", password="password") # `region="ous"` if outside of US, `region="jp"` if Japan
>>> dexcom = Dexcom(username="+11234567890", password="password") # phone number
>>> dexcom = Dexcom(username="user@email.com", password="password") # email address
>>> dexcom = Dexcom(account_id="12345678-90ab-cdef-1234-567890abcdef", password="password") # account ID (advanced)
>>> glucose_reading = dexcom.get_current_glucose_reading()
>>> print(glucose_reading)
85
>>> glucose_reading.value
85
>>> glucose_reading.mmol_l
4.7
>>> glucose_reading.trend
4
>>> glucose_reading.trend_direction
'Flat'
>>> glucose_reading.trend_description
'steady'
>>> glucose_reading.trend_arrow
'→'
>>> print(bg.datetime)
2023-08-07 20:40:58
>>> glucose_reading.json
{'WT': 'Date(1691455258000)', 'ST': 'Date(1691455258000)', 'DT': 'Date(1691455258000-0400)', 'Value': 85, 'Trend': 'Flat'}
Documentation
https://gagebenne.github.io/pydexcom/pydexcom.html
Frequently Asked Questions
Why is my password not working?
The Dexcom Share API understandably reports limited information during account validation. If anything is incorrect, the API simply reports back invalid password ( pydexcom.errors.AccountErrorEnum
). However, there could be many reasons you are getting this error:
1. Ensure your credentials are valid.
Validate your Dexcom account credentials by logging on to the Dexcom Account Management website for your region:
For users in the United States: uam1.dexcom.com. For users outside of the United States: uam2.dexcom.com. For users in the Asia-Pacific: uam.dexcom.jp.
2. Use the correct Dexcom Share API endpoint.
For users in the United States: use the default, or set region="us"
when initializing Dexcom
.
For users outside of the United States: be sure to set region="ous"
when initializing Dexcom
.
For users in Japan: be sure to set region="jp"
when initializing Dexcom
.
3. Ensure your username is correctly formatted.
Format phone numbers with a +
, your country code, then your phone number. For example, a US phone number of (123)-456-7890
would be supplied as a username="+11234567890"
.
4. Use _your_ Dexcom Share credentials, not the _follower's_ credentials.
Use the same credentials used to login to the Dexcom mobile application publishing the glucose readings.
5. Ensure you have at least one follower on Dexcom Share.
The Dexcom Share service requires setup of at least one follower to enable the service, as does this package.
6. Try using your account ID.
You can find your account ID by logging in to Dexcom Account Management website for your region. After logging in, note the UUID in the URL -- this is your account ID.
Format account IDs (UUIDs) with hyphens. For example, an account ID of 1234567890abcdef1234567890abcdef
found in the URL after logging in would be supplied as account_id="12345678-90ab-cdef-1234-567890abcdef"
.
7. Report it!
The Dexcom Share API sometimes changes. If you believe there is an issue with pydexcom
, feel free to create an issue if one has not been created yet already.
Why not use the official Dexcom Developer API?
The official Dexcom API is a great tool to view trends, statistics, and day-by-day data, but is not suitable for real time fetching of glucose readings as it is a retrospective API.
Can I use the Dexcom Stelo with this package?
No, the Dexcom Stelo isn't compatible with the Dexcom Share service, so this package can't retrieve its readings.
How can I let you know of suggestions or issues?
By all means submit a pull request if you have a feature you would like to see in the next release. Alternatively, you may create an issue if you have a suggestion or bug you'd like to report.
Where is this package being used?
Primarily this package is used in the Home Assistant Dexcom integration, but it's fantastic to see community projects involving pydexcom
:
1""" 2.. include:: ../README.md 3""" # noqa: D200, D212, D400, D415 4 5from __future__ import annotations 6 7import logging 8import re 9from datetime import datetime 10from typing import Any 11from uuid import UUID 12 13import requests 14 15from .const import ( 16 DEFAULT_UUID, 17 DEXCOM_APPLICATION_IDS, 18 DEXCOM_AUTHENTICATE_ENDPOINT, 19 DEXCOM_BASE_URLS, 20 DEXCOM_GLUCOSE_READINGS_ENDPOINT, 21 DEXCOM_LOGIN_ID_ENDPOINT, 22 DEXCOM_TREND_DIRECTIONS, 23 MAX_MAX_COUNT, 24 MAX_MINUTES, 25 MMOL_L_CONVERSION_FACTOR, 26 TREND_ARROWS, 27 TREND_DESCRIPTIONS, 28 Region, 29) 30from .errors import ( 31 AccountError, 32 AccountErrorEnum, 33 ArgumentError, 34 ArgumentErrorEnum, 35 DexcomError, 36 SessionError, 37 SessionErrorEnum, 38) 39 40_LOGGER = logging.getLogger("pydexcom") 41 42 43class GlucoseReading: 44 """Class for parsing glucose reading from Dexcom Share API.""" 45 46 def __init__(self, json_glucose_reading: dict[str, Any]) -> None: 47 """Initialize `GlucoseReading` with JSON glucose reading from Dexcom Share API. 48 49 :param json_glucose_reading: JSON glucose reading from Dexcom Share API 50 """ 51 self._json = json_glucose_reading 52 try: 53 self._value = int(json_glucose_reading["Value"]) 54 self._trend_direction: str = json_glucose_reading["Trend"] 55 # Dexcom Share API returns `str` direction now, previously `int` trend 56 self._trend: int = DEXCOM_TREND_DIRECTIONS[self._trend_direction] 57 58 match = re.match( 59 r"Date\((?P<timestamp>\d+)(?P<timezone>[+-]\d{4})\)", 60 json_glucose_reading["DT"], 61 ) 62 if match: 63 self._datetime = datetime.fromtimestamp( 64 int(match.group("timestamp")) / 1000.0, 65 tz=datetime.strptime(match.group("timezone"), "%z").tzinfo, 66 ) 67 except (KeyError, TypeError, ValueError) as error: 68 raise ArgumentError(ArgumentErrorEnum.GLUCOSE_READING_INVALID) from error 69 70 @property 71 def value(self) -> int: 72 """Blood glucose value in mg/dL.""" 73 return self._value 74 75 @property 76 def mg_dl(self) -> int: 77 """Blood glucose value in mg/dL.""" 78 return self._value 79 80 @property 81 def mmol_l(self) -> float: 82 """Blood glucose value in mmol/L.""" 83 return round(self.value * MMOL_L_CONVERSION_FACTOR, 1) 84 85 @property 86 def trend(self) -> int: 87 """Blood glucose trend information. 88 89 Value of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`. 90 """ 91 return self._trend 92 93 @property 94 def trend_direction(self) -> str: 95 """Blood glucose trend direction. 96 97 Key of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`. 98 """ 99 return self._trend_direction 100 101 @property 102 def trend_description(self) -> str | None: 103 """Blood glucose trend information description. 104 105 See `pydexcom.const.TREND_DESCRIPTIONS`. 106 """ 107 return TREND_DESCRIPTIONS[self._trend] 108 109 @property 110 def trend_arrow(self) -> str: 111 """Blood glucose trend as unicode arrow (`pydexcom.const.TREND_ARROWS`).""" 112 return TREND_ARROWS[self._trend] 113 114 @property 115 def datetime(self) -> datetime: 116 """Glucose reading recorded time as datetime.""" 117 return self._datetime 118 119 @property 120 def json(self) -> dict[str, Any]: 121 """JSON glucose reading from Dexcom Share API.""" 122 return self._json 123 124 def __str__(self) -> str: 125 """Blood glucose value as in mg/dL.""" 126 return str(self._value) 127 128 129def valid_uuid(uuid: str | None) -> bool: 130 """Check if UUID is valid.""" 131 try: 132 UUID(str(uuid)) 133 except ValueError: 134 return False 135 else: 136 return True 137 138 139class Dexcom: 140 """Class for communicating with Dexcom Share API.""" 141 142 def __init__( 143 self, 144 *, 145 password: str, 146 account_id: str | None = None, 147 username: str | None = None, 148 region: Region = Region.US, 149 ) -> None: 150 """Initialize `Dexcom` with Dexcom Share credentials. 151 152 :param username: username for the Dexcom Share user, *not follower*. 153 :param account_id: account ID for the Dexcom Share user, *not follower*. 154 :param password: password for the Dexcom Share user. 155 :param region: the region to use, one of `"us"`, `"ous"`, `"jp"`. 156 """ 157 user_ids = sum(user_id is not None for user_id in [account_id, username]) 158 if user_ids == 0: 159 raise ArgumentError(ArgumentErrorEnum.NONE_USER_ID_PROVIDED) 160 if user_ids != 1: 161 raise ArgumentError(ArgumentErrorEnum.TOO_MANY_USER_ID_PROVIDED) 162 163 self._base_url = DEXCOM_BASE_URLS[region] 164 self._application_id = DEXCOM_APPLICATION_IDS[region] 165 self._password = password 166 self._username: str | None = username 167 self._account_id: str | None = account_id 168 self._session_id: str | None = None 169 self.__session = requests.Session() 170 self._session() 171 172 def _post( 173 self, 174 endpoint: str, 175 params: dict[str, Any] | None = None, 176 json: dict[str, Any] | None = None, 177 ) -> Any: # noqa: ANN401 178 """Send post request to Dexcom Share API. 179 180 :param endpoint: URL of the post request 181 :param params: `dict` to send in the query string of the post request 182 :param json: JSON to send in the body of the post request 183 """ 184 response = self.__session.post( 185 f"{self._base_url}/{endpoint}", 186 headers={"Accept-Encoding": "application/json"}, 187 params=params, 188 json={} if json is None else json, 189 ) 190 191 try: 192 response.raise_for_status() 193 return response.json() 194 except requests.HTTPError as http_error: 195 error = self._handle_response(response) 196 if error: 197 raise error from http_error 198 _LOGGER.exception("%s", response.text) 199 raise 200 201 def _handle_response(self, response: requests.Response) -> DexcomError | None: # noqa: C901 202 error: DexcomError | None = None 203 """ 204 Parse `requests.Response` for `pydexcom.errors.DexcomError`. 205 206 :param response: `requests.Response` to parse 207 """ 208 if response.json(): 209 _LOGGER.debug("%s", response.json()) 210 code = response.json().get("Code", None) 211 message = response.json().get("Message", None) 212 if code == "SessionIdNotFound": 213 error = SessionError(SessionErrorEnum.NOT_FOUND) 214 elif code == "SessionNotValid": 215 error = SessionError(SessionErrorEnum.INVALID) 216 elif code == "AccountPasswordInvalid": # defunct 217 error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) 218 elif code == "SSO_AuthenticateMaxAttemptsExceeded": 219 error = AccountError(AccountErrorEnum.MAX_ATTEMPTS) 220 elif code == "SSO_InternalError": 221 if message and ( 222 "Cannot Authenticate by AccountName" in message 223 or "Cannot Authenticate by AccountId" in message 224 ): 225 error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) 226 elif code == "InvalidArgument": 227 if message and "accountName" in message: 228 error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) 229 elif message and "password" in message: 230 error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) 231 elif message and "UUID" in message: 232 error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) 233 elif code and message: 234 _LOGGER.debug("%s: %s", code, message) 235 return error 236 237 def _validate_session_id(self) -> None: 238 """Validate session ID.""" 239 if any( 240 [ 241 not isinstance(self._session_id, str), 242 not self._session_id, 243 not valid_uuid(self._session_id), 244 ], 245 ): 246 raise ArgumentError(ArgumentErrorEnum.SESSION_ID_INVALID) 247 if self._session_id == DEFAULT_UUID: 248 raise ArgumentError(ArgumentErrorEnum.SESSION_ID_DEFAULT) 249 250 def _validate_username(self) -> None: 251 """Validate username.""" 252 if any([not isinstance(self._username, str), not self._username]): 253 raise ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) 254 255 def _validate_password(self) -> None: 256 """Validate password.""" 257 if any([not isinstance(self._password, str), not self._password]): 258 raise ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) 259 260 def _validate_account_id(self) -> None: 261 """Validate account ID.""" 262 if any( 263 [ 264 not isinstance(self._account_id, str), 265 not self._account_id, 266 not valid_uuid(self._account_id), 267 ], 268 ): 269 raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) 270 if self._account_id == DEFAULT_UUID: 271 raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_DEFAULT) 272 273 def _get_account_id(self) -> str: 274 """Retrieve account ID from the authentication endpoint. 275 276 See `pydexcom.const.DEXCOM_AUTHENTICATE_ENDPOINT`. 277 """ 278 _LOGGER.debug("Retrieve account ID from the authentication endpoint") 279 return self._post( 280 DEXCOM_AUTHENTICATE_ENDPOINT, 281 json={ 282 "accountName": self._username, 283 "password": self._password, 284 "applicationId": self._application_id, 285 }, 286 ) 287 288 def _get_session_id(self) -> str: 289 """Retrieve session ID from the login endpoint. 290 291 See `pydexcom.const.DEXCOM_LOGIN_ID_ENDPOINT`. 292 """ 293 _LOGGER.debug("Retrieve session ID from the login endpoint") 294 return self._post( 295 DEXCOM_LOGIN_ID_ENDPOINT, 296 json={ 297 "accountId": self._account_id, 298 "password": self._password, 299 "applicationId": self._application_id, 300 }, 301 ) 302 303 def _session(self) -> None: 304 """Create Dexcom Share API session.""" 305 self._validate_password() 306 307 if self._account_id is None: 308 self._validate_username() 309 self._account_id = self._get_account_id() 310 311 self._validate_account_id() 312 self._session_id = self._get_session_id() 313 self._validate_session_id() 314 315 def _get_glucose_readings( 316 self, 317 minutes: int = MAX_MINUTES, 318 max_count: int = MAX_MAX_COUNT, 319 ) -> list[dict[str, Any]]: 320 """Retrieve glucose readings from the glucose readings endpoint. 321 322 See `pydexcom.const.DEXCOM_GLUCOSE_READINGS_ENDPOINT`. 323 """ 324 if not isinstance(minutes, int) or any([minutes < 0, minutes > MAX_MINUTES]): 325 raise ArgumentError(ArgumentErrorEnum.MINUTES_INVALID) 326 if not isinstance(max_count, int) or any( 327 [max_count < 0, max_count > MAX_MAX_COUNT], 328 ): 329 raise ArgumentError(ArgumentErrorEnum.MAX_COUNT_INVALID) 330 331 _LOGGER.debug("Retrieve glucose readings from the glucose readings endpoint") 332 return self._post( 333 DEXCOM_GLUCOSE_READINGS_ENDPOINT, 334 params={ 335 "sessionId": self._session_id, 336 "minutes": minutes, 337 "maxCount": max_count, 338 }, 339 ) 340 341 def get_glucose_readings( 342 self, 343 minutes: int = MAX_MINUTES, 344 max_count: int = MAX_MAX_COUNT, 345 ) -> list[GlucoseReading]: 346 """Get `max_count` glucose readings within specified number of `minutes`. 347 348 Catches one instance of a thrown `pydexcom.errors.SessionError` if session ID 349 expired, attempts to get a new session ID and retries. 350 351 :param minutes: Number of minutes to retrieve glucose readings from (1-1440) 352 :param max_count: Maximum number of glucose readings to retrieve (1-288) 353 """ 354 json_glucose_readings: list[dict[str, Any]] = [] 355 356 try: 357 # Requesting glucose reading with DEFAULT_UUID returns non-JSON empty string 358 self._validate_session_id() 359 360 json_glucose_readings = self._get_glucose_readings(minutes, max_count) 361 except SessionError: 362 # Attempt to update expired session ID 363 self._session() 364 365 json_glucose_readings = self._get_glucose_readings(minutes, max_count) 366 367 return [GlucoseReading(json_reading) for json_reading in json_glucose_readings] 368 369 def get_latest_glucose_reading(self) -> GlucoseReading | None: 370 """Get latest available glucose reading, within the last 24 hours.""" 371 glucose_readings = self.get_glucose_readings(max_count=1) 372 return glucose_readings[0] if glucose_readings else None 373 374 def get_current_glucose_reading(self) -> GlucoseReading | None: 375 """Get current available glucose reading, within the last 10 minutes.""" 376 glucose_readings = self.get_glucose_readings(minutes=10, max_count=1) 377 return glucose_readings[0] if glucose_readings else None
44class GlucoseReading: 45 """Class for parsing glucose reading from Dexcom Share API.""" 46 47 def __init__(self, json_glucose_reading: dict[str, Any]) -> None: 48 """Initialize `GlucoseReading` with JSON glucose reading from Dexcom Share API. 49 50 :param json_glucose_reading: JSON glucose reading from Dexcom Share API 51 """ 52 self._json = json_glucose_reading 53 try: 54 self._value = int(json_glucose_reading["Value"]) 55 self._trend_direction: str = json_glucose_reading["Trend"] 56 # Dexcom Share API returns `str` direction now, previously `int` trend 57 self._trend: int = DEXCOM_TREND_DIRECTIONS[self._trend_direction] 58 59 match = re.match( 60 r"Date\((?P<timestamp>\d+)(?P<timezone>[+-]\d{4})\)", 61 json_glucose_reading["DT"], 62 ) 63 if match: 64 self._datetime = datetime.fromtimestamp( 65 int(match.group("timestamp")) / 1000.0, 66 tz=datetime.strptime(match.group("timezone"), "%z").tzinfo, 67 ) 68 except (KeyError, TypeError, ValueError) as error: 69 raise ArgumentError(ArgumentErrorEnum.GLUCOSE_READING_INVALID) from error 70 71 @property 72 def value(self) -> int: 73 """Blood glucose value in mg/dL.""" 74 return self._value 75 76 @property 77 def mg_dl(self) -> int: 78 """Blood glucose value in mg/dL.""" 79 return self._value 80 81 @property 82 def mmol_l(self) -> float: 83 """Blood glucose value in mmol/L.""" 84 return round(self.value * MMOL_L_CONVERSION_FACTOR, 1) 85 86 @property 87 def trend(self) -> int: 88 """Blood glucose trend information. 89 90 Value of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`. 91 """ 92 return self._trend 93 94 @property 95 def trend_direction(self) -> str: 96 """Blood glucose trend direction. 97 98 Key of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`. 99 """ 100 return self._trend_direction 101 102 @property 103 def trend_description(self) -> str | None: 104 """Blood glucose trend information description. 105 106 See `pydexcom.const.TREND_DESCRIPTIONS`. 107 """ 108 return TREND_DESCRIPTIONS[self._trend] 109 110 @property 111 def trend_arrow(self) -> str: 112 """Blood glucose trend as unicode arrow (`pydexcom.const.TREND_ARROWS`).""" 113 return TREND_ARROWS[self._trend] 114 115 @property 116 def datetime(self) -> datetime: 117 """Glucose reading recorded time as datetime.""" 118 return self._datetime 119 120 @property 121 def json(self) -> dict[str, Any]: 122 """JSON glucose reading from Dexcom Share API.""" 123 return self._json 124 125 def __str__(self) -> str: 126 """Blood glucose value as in mg/dL.""" 127 return str(self._value)
Class for parsing glucose reading from Dexcom Share API.
47 def __init__(self, json_glucose_reading: dict[str, Any]) -> None: 48 """Initialize `GlucoseReading` with JSON glucose reading from Dexcom Share API. 49 50 :param json_glucose_reading: JSON glucose reading from Dexcom Share API 51 """ 52 self._json = json_glucose_reading 53 try: 54 self._value = int(json_glucose_reading["Value"]) 55 self._trend_direction: str = json_glucose_reading["Trend"] 56 # Dexcom Share API returns `str` direction now, previously `int` trend 57 self._trend: int = DEXCOM_TREND_DIRECTIONS[self._trend_direction] 58 59 match = re.match( 60 r"Date\((?P<timestamp>\d+)(?P<timezone>[+-]\d{4})\)", 61 json_glucose_reading["DT"], 62 ) 63 if match: 64 self._datetime = datetime.fromtimestamp( 65 int(match.group("timestamp")) / 1000.0, 66 tz=datetime.strptime(match.group("timezone"), "%z").tzinfo, 67 ) 68 except (KeyError, TypeError, ValueError) as error: 69 raise ArgumentError(ArgumentErrorEnum.GLUCOSE_READING_INVALID) from error
Initialize GlucoseReading
with JSON glucose reading from Dexcom Share API.
Parameters
- json_glucose_reading: JSON glucose reading from Dexcom Share API
71 @property 72 def value(self) -> int: 73 """Blood glucose value in mg/dL.""" 74 return self._value
Blood glucose value in mg/dL.
76 @property 77 def mg_dl(self) -> int: 78 """Blood glucose value in mg/dL.""" 79 return self._value
Blood glucose value in mg/dL.
81 @property 82 def mmol_l(self) -> float: 83 """Blood glucose value in mmol/L.""" 84 return round(self.value * MMOL_L_CONVERSION_FACTOR, 1)
Blood glucose value in mmol/L.
86 @property 87 def trend(self) -> int: 88 """Blood glucose trend information. 89 90 Value of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`. 91 """ 92 return self._trend
Blood glucose trend information.
Value of pydexcom.const.DEXCOM_TREND_DIRECTIONS
.
94 @property 95 def trend_direction(self) -> str: 96 """Blood glucose trend direction. 97 98 Key of `pydexcom.const.DEXCOM_TREND_DIRECTIONS`. 99 """ 100 return self._trend_direction
Blood glucose trend direction.
102 @property 103 def trend_description(self) -> str | None: 104 """Blood glucose trend information description. 105 106 See `pydexcom.const.TREND_DESCRIPTIONS`. 107 """ 108 return TREND_DESCRIPTIONS[self._trend]
Blood glucose trend information description.
110 @property 111 def trend_arrow(self) -> str: 112 """Blood glucose trend as unicode arrow (`pydexcom.const.TREND_ARROWS`).""" 113 return TREND_ARROWS[self._trend]
Blood glucose trend as unicode arrow (pydexcom.const.TREND_ARROWS
).
130def valid_uuid(uuid: str | None) -> bool: 131 """Check if UUID is valid.""" 132 try: 133 UUID(str(uuid)) 134 except ValueError: 135 return False 136 else: 137 return True
Check if UUID is valid.
140class Dexcom: 141 """Class for communicating with Dexcom Share API.""" 142 143 def __init__( 144 self, 145 *, 146 password: str, 147 account_id: str | None = None, 148 username: str | None = None, 149 region: Region = Region.US, 150 ) -> None: 151 """Initialize `Dexcom` with Dexcom Share credentials. 152 153 :param username: username for the Dexcom Share user, *not follower*. 154 :param account_id: account ID for the Dexcom Share user, *not follower*. 155 :param password: password for the Dexcom Share user. 156 :param region: the region to use, one of `"us"`, `"ous"`, `"jp"`. 157 """ 158 user_ids = sum(user_id is not None for user_id in [account_id, username]) 159 if user_ids == 0: 160 raise ArgumentError(ArgumentErrorEnum.NONE_USER_ID_PROVIDED) 161 if user_ids != 1: 162 raise ArgumentError(ArgumentErrorEnum.TOO_MANY_USER_ID_PROVIDED) 163 164 self._base_url = DEXCOM_BASE_URLS[region] 165 self._application_id = DEXCOM_APPLICATION_IDS[region] 166 self._password = password 167 self._username: str | None = username 168 self._account_id: str | None = account_id 169 self._session_id: str | None = None 170 self.__session = requests.Session() 171 self._session() 172 173 def _post( 174 self, 175 endpoint: str, 176 params: dict[str, Any] | None = None, 177 json: dict[str, Any] | None = None, 178 ) -> Any: # noqa: ANN401 179 """Send post request to Dexcom Share API. 180 181 :param endpoint: URL of the post request 182 :param params: `dict` to send in the query string of the post request 183 :param json: JSON to send in the body of the post request 184 """ 185 response = self.__session.post( 186 f"{self._base_url}/{endpoint}", 187 headers={"Accept-Encoding": "application/json"}, 188 params=params, 189 json={} if json is None else json, 190 ) 191 192 try: 193 response.raise_for_status() 194 return response.json() 195 except requests.HTTPError as http_error: 196 error = self._handle_response(response) 197 if error: 198 raise error from http_error 199 _LOGGER.exception("%s", response.text) 200 raise 201 202 def _handle_response(self, response: requests.Response) -> DexcomError | None: # noqa: C901 203 error: DexcomError | None = None 204 """ 205 Parse `requests.Response` for `pydexcom.errors.DexcomError`. 206 207 :param response: `requests.Response` to parse 208 """ 209 if response.json(): 210 _LOGGER.debug("%s", response.json()) 211 code = response.json().get("Code", None) 212 message = response.json().get("Message", None) 213 if code == "SessionIdNotFound": 214 error = SessionError(SessionErrorEnum.NOT_FOUND) 215 elif code == "SessionNotValid": 216 error = SessionError(SessionErrorEnum.INVALID) 217 elif code == "AccountPasswordInvalid": # defunct 218 error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) 219 elif code == "SSO_AuthenticateMaxAttemptsExceeded": 220 error = AccountError(AccountErrorEnum.MAX_ATTEMPTS) 221 elif code == "SSO_InternalError": 222 if message and ( 223 "Cannot Authenticate by AccountName" in message 224 or "Cannot Authenticate by AccountId" in message 225 ): 226 error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) 227 elif code == "InvalidArgument": 228 if message and "accountName" in message: 229 error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) 230 elif message and "password" in message: 231 error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) 232 elif message and "UUID" in message: 233 error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) 234 elif code and message: 235 _LOGGER.debug("%s: %s", code, message) 236 return error 237 238 def _validate_session_id(self) -> None: 239 """Validate session ID.""" 240 if any( 241 [ 242 not isinstance(self._session_id, str), 243 not self._session_id, 244 not valid_uuid(self._session_id), 245 ], 246 ): 247 raise ArgumentError(ArgumentErrorEnum.SESSION_ID_INVALID) 248 if self._session_id == DEFAULT_UUID: 249 raise ArgumentError(ArgumentErrorEnum.SESSION_ID_DEFAULT) 250 251 def _validate_username(self) -> None: 252 """Validate username.""" 253 if any([not isinstance(self._username, str), not self._username]): 254 raise ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) 255 256 def _validate_password(self) -> None: 257 """Validate password.""" 258 if any([not isinstance(self._password, str), not self._password]): 259 raise ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) 260 261 def _validate_account_id(self) -> None: 262 """Validate account ID.""" 263 if any( 264 [ 265 not isinstance(self._account_id, str), 266 not self._account_id, 267 not valid_uuid(self._account_id), 268 ], 269 ): 270 raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) 271 if self._account_id == DEFAULT_UUID: 272 raise ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_DEFAULT) 273 274 def _get_account_id(self) -> str: 275 """Retrieve account ID from the authentication endpoint. 276 277 See `pydexcom.const.DEXCOM_AUTHENTICATE_ENDPOINT`. 278 """ 279 _LOGGER.debug("Retrieve account ID from the authentication endpoint") 280 return self._post( 281 DEXCOM_AUTHENTICATE_ENDPOINT, 282 json={ 283 "accountName": self._username, 284 "password": self._password, 285 "applicationId": self._application_id, 286 }, 287 ) 288 289 def _get_session_id(self) -> str: 290 """Retrieve session ID from the login endpoint. 291 292 See `pydexcom.const.DEXCOM_LOGIN_ID_ENDPOINT`. 293 """ 294 _LOGGER.debug("Retrieve session ID from the login endpoint") 295 return self._post( 296 DEXCOM_LOGIN_ID_ENDPOINT, 297 json={ 298 "accountId": self._account_id, 299 "password": self._password, 300 "applicationId": self._application_id, 301 }, 302 ) 303 304 def _session(self) -> None: 305 """Create Dexcom Share API session.""" 306 self._validate_password() 307 308 if self._account_id is None: 309 self._validate_username() 310 self._account_id = self._get_account_id() 311 312 self._validate_account_id() 313 self._session_id = self._get_session_id() 314 self._validate_session_id() 315 316 def _get_glucose_readings( 317 self, 318 minutes: int = MAX_MINUTES, 319 max_count: int = MAX_MAX_COUNT, 320 ) -> list[dict[str, Any]]: 321 """Retrieve glucose readings from the glucose readings endpoint. 322 323 See `pydexcom.const.DEXCOM_GLUCOSE_READINGS_ENDPOINT`. 324 """ 325 if not isinstance(minutes, int) or any([minutes < 0, minutes > MAX_MINUTES]): 326 raise ArgumentError(ArgumentErrorEnum.MINUTES_INVALID) 327 if not isinstance(max_count, int) or any( 328 [max_count < 0, max_count > MAX_MAX_COUNT], 329 ): 330 raise ArgumentError(ArgumentErrorEnum.MAX_COUNT_INVALID) 331 332 _LOGGER.debug("Retrieve glucose readings from the glucose readings endpoint") 333 return self._post( 334 DEXCOM_GLUCOSE_READINGS_ENDPOINT, 335 params={ 336 "sessionId": self._session_id, 337 "minutes": minutes, 338 "maxCount": max_count, 339 }, 340 ) 341 342 def get_glucose_readings( 343 self, 344 minutes: int = MAX_MINUTES, 345 max_count: int = MAX_MAX_COUNT, 346 ) -> list[GlucoseReading]: 347 """Get `max_count` glucose readings within specified number of `minutes`. 348 349 Catches one instance of a thrown `pydexcom.errors.SessionError` if session ID 350 expired, attempts to get a new session ID and retries. 351 352 :param minutes: Number of minutes to retrieve glucose readings from (1-1440) 353 :param max_count: Maximum number of glucose readings to retrieve (1-288) 354 """ 355 json_glucose_readings: list[dict[str, Any]] = [] 356 357 try: 358 # Requesting glucose reading with DEFAULT_UUID returns non-JSON empty string 359 self._validate_session_id() 360 361 json_glucose_readings = self._get_glucose_readings(minutes, max_count) 362 except SessionError: 363 # Attempt to update expired session ID 364 self._session() 365 366 json_glucose_readings = self._get_glucose_readings(minutes, max_count) 367 368 return [GlucoseReading(json_reading) for json_reading in json_glucose_readings] 369 370 def get_latest_glucose_reading(self) -> GlucoseReading | None: 371 """Get latest available glucose reading, within the last 24 hours.""" 372 glucose_readings = self.get_glucose_readings(max_count=1) 373 return glucose_readings[0] if glucose_readings else None 374 375 def get_current_glucose_reading(self) -> GlucoseReading | None: 376 """Get current available glucose reading, within the last 10 minutes.""" 377 glucose_readings = self.get_glucose_readings(minutes=10, max_count=1) 378 return glucose_readings[0] if glucose_readings else None
Class for communicating with Dexcom Share API.
143 def __init__( 144 self, 145 *, 146 password: str, 147 account_id: str | None = None, 148 username: str | None = None, 149 region: Region = Region.US, 150 ) -> None: 151 """Initialize `Dexcom` with Dexcom Share credentials. 152 153 :param username: username for the Dexcom Share user, *not follower*. 154 :param account_id: account ID for the Dexcom Share user, *not follower*. 155 :param password: password for the Dexcom Share user. 156 :param region: the region to use, one of `"us"`, `"ous"`, `"jp"`. 157 """ 158 user_ids = sum(user_id is not None for user_id in [account_id, username]) 159 if user_ids == 0: 160 raise ArgumentError(ArgumentErrorEnum.NONE_USER_ID_PROVIDED) 161 if user_ids != 1: 162 raise ArgumentError(ArgumentErrorEnum.TOO_MANY_USER_ID_PROVIDED) 163 164 self._base_url = DEXCOM_BASE_URLS[region] 165 self._application_id = DEXCOM_APPLICATION_IDS[region] 166 self._password = password 167 self._username: str | None = username 168 self._account_id: str | None = account_id 169 self._session_id: str | None = None 170 self.__session = requests.Session() 171 self._session()
Initialize Dexcom
with Dexcom Share credentials.
Parameters
- username: username for the Dexcom Share user, not follower.
- account_id: account ID for the Dexcom Share user, not follower.
- password: password for the Dexcom Share user.
- region: the region to use, one of
"us"
,"ous"
,"jp"
.
342 def get_glucose_readings( 343 self, 344 minutes: int = MAX_MINUTES, 345 max_count: int = MAX_MAX_COUNT, 346 ) -> list[GlucoseReading]: 347 """Get `max_count` glucose readings within specified number of `minutes`. 348 349 Catches one instance of a thrown `pydexcom.errors.SessionError` if session ID 350 expired, attempts to get a new session ID and retries. 351 352 :param minutes: Number of minutes to retrieve glucose readings from (1-1440) 353 :param max_count: Maximum number of glucose readings to retrieve (1-288) 354 """ 355 json_glucose_readings: list[dict[str, Any]] = [] 356 357 try: 358 # Requesting glucose reading with DEFAULT_UUID returns non-JSON empty string 359 self._validate_session_id() 360 361 json_glucose_readings = self._get_glucose_readings(minutes, max_count) 362 except SessionError: 363 # Attempt to update expired session ID 364 self._session() 365 366 json_glucose_readings = self._get_glucose_readings(minutes, max_count) 367 368 return [GlucoseReading(json_reading) for json_reading in json_glucose_readings]
Get max_count
glucose readings within specified number of minutes
.
Catches one instance of a thrown pydexcom.errors.SessionError
if session ID
expired, attempts to get a new session ID and retries.
Parameters
- minutes: Number of minutes to retrieve glucose readings from (1-1440)
- max_count: Maximum number of glucose readings to retrieve (1-288)
370 def get_latest_glucose_reading(self) -> GlucoseReading | None: 371 """Get latest available glucose reading, within the last 24 hours.""" 372 glucose_readings = self.get_glucose_readings(max_count=1) 373 return glucose_readings[0] if glucose_readings else None
Get latest available glucose reading, within the last 24 hours.
375 def get_current_glucose_reading(self) -> GlucoseReading | None: 376 """Get current available glucose reading, within the last 10 minutes.""" 377 glucose_readings = self.get_glucose_readings(minutes=10, max_count=1) 378 return glucose_readings[0] if glucose_readings else None
Get current available glucose reading, within the last 10 minutes.