mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
feat: Add EU region support for Tandem t:connect API
Add comprehensive support for European Tandem t:connect servers alongside existing US support. Features: - Region parameter in TandemSourceApi (default: US, supports: US/EU) - EU-specific API endpoints and client ID configuration - Region-aware credential caching to prevent cross-region conflicts - Command line --region flag and TCONNECT_REGION environment variable - Full backward compatibility (defaults to US region) Technical Changes: - Separated common SSO URLs from region-specific service URLs - Added region validation and URL property methods - Enhanced credential cache with region isolation - Updated CLI argument parsing and configuration system - Added comprehensive logging for region selection Testing: - Verified US region backward compatibility - Successfully tested EU authentication and data retrieval - Processed real EU pump data (1500+ events, 39KB) - Validated all event types: basal, bolus, CGM, user modes, alarms - Confirmed Nightscout integration compatibility This enables EU Tandem pump users to sync their data using: --region EU or TCONNECT_REGION=EU
This commit is contained in:
committed by
James Woglom
parent
803b15b886
commit
4888426034
@@ -25,6 +25,7 @@ try:
|
||||
from .secret import (
|
||||
TCONNECT_EMAIL,
|
||||
TCONNECT_PASSWORD,
|
||||
TCONNECT_REGION,
|
||||
NS_URL,
|
||||
NS_SECRET,
|
||||
NS_SKIP_TLS_VERIFY,
|
||||
@@ -54,6 +55,7 @@ def parse_args(*args, **kwargs):
|
||||
parser.add_argument('--check-login', dest='check_login', action='store_const', const=True, default=False, help='If set, checks that the provided t:connect credentials can be used to log in.')
|
||||
parser.add_argument('--features', dest='features', nargs='+', default=DEFAULT_FEATURES, choices=ALL_FEATURES, help='Specifies what data should be synchronized between tconnect and Nightscout.')
|
||||
parser.add_argument('--tandem-source', dest='tandem_source', action='store_const', const=True, default=False, help='FOR TESTING: Use Tandem Source')
|
||||
parser.add_argument('--region', dest='region', type=str, choices=['US', 'EU'], default=None, help='Tandem t:connect server region (US or EU). If not specified, uses TCONNECT_REGION from configuration or defaults to US.')
|
||||
|
||||
return parser.parse_args(*args, **kwargs)
|
||||
|
||||
@@ -85,6 +87,8 @@ def main(*args, **kwargs):
|
||||
if time_end < time_start:
|
||||
raise Exception('time_start must be before time_end')
|
||||
|
||||
# Determine region: command line arg takes precedence, then config, then default to US
|
||||
region = args.region if args.region else TCONNECT_REGION
|
||||
|
||||
if TCONNECT_EMAIL == 'email@email.com':
|
||||
logging.warn('NO USERNAME WAS PROVIDED. Ensure you have set TCONNECT_EMAIL appropriately.')
|
||||
@@ -98,7 +102,7 @@ def main(*args, **kwargs):
|
||||
else:
|
||||
logging.warn('NO PUMP SERIAL NUMBER WAS PROVIDED. Ensure you have set PUMP_SERIAL_NUMBER appropriately.')
|
||||
|
||||
tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
|
||||
tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD, region)
|
||||
|
||||
nightscout = NightscoutApi(NS_URL, NS_SECRET, skip_verify=NS_SKIP_TLS_VERIFY, ignore_conn_errors=NS_IGNORE_CONN_ERRORS)
|
||||
|
||||
@@ -110,6 +114,7 @@ def main(*args, **kwargs):
|
||||
logging.info("You may notice different behavior compared to older versions which utilized t:connect data sources.")
|
||||
logging.info("To report a bug or to get help, see https://github.com/jwoglom/tconnectsync/issues")
|
||||
|
||||
logging.info(f"Using Tandem t:connect region: {region}")
|
||||
logging.info("Enabled features: " + ", ".join(args.features))
|
||||
|
||||
if args.check_login:
|
||||
|
||||
@@ -13,9 +13,10 @@ class TConnectApi:
|
||||
email = None
|
||||
password = None
|
||||
|
||||
def __init__(self, email, password):
|
||||
def __init__(self, email, password, region='US'):
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.region = region
|
||||
self._ciq = None
|
||||
self._ws2 = None
|
||||
self._android = None
|
||||
@@ -27,9 +28,9 @@ class TConnectApi:
|
||||
if self._tandemsource and not self._tandemsource.needs_relogin():
|
||||
return self._tandemsource
|
||||
|
||||
logger.debug("Instantiating new TandemSourceApi")
|
||||
logger.debug(f"Instantiating new TandemSourceApi for region {self.region}")
|
||||
|
||||
self._tandemsource = TandemSourceApi(self.email, self.password)
|
||||
self._tandemsource = TandemSourceApi(self.email, self.password, self.region)
|
||||
return self._tandemsource
|
||||
|
||||
|
||||
|
||||
@@ -24,23 +24,73 @@ from ..eventparser.generic import Events, decode_raw_events, EVENT_LEN
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TandemSourceApi:
|
||||
# Common URLs that are shared between regions
|
||||
LOGIN_PAGE_URL = 'https://sso.tandemdiabetes.com/'
|
||||
LOGIN_API_URL = 'https://tdcservices.tandemdiabetes.com/accounts/api/login'
|
||||
TDC_AUTH_CALLBACK_URL = 'https://sso.tandemdiabetes.com/auth/callback'
|
||||
TDC_OAUTH_AUTHORIZE_URL = 'https://tdcservices.tandemdiabetes.com/accounts/api/oauth2/v1/authorize'
|
||||
TDC_OIDC_JWKS_URL = 'https://tdcservices.tandemdiabetes.com/accounts/api/.well-known/openid-configuration/jwks'
|
||||
TDC_OIDC_ISSUER = 'https://tdcservices.tandemdiabetes.com/accounts/api' # openid_config['issuer']
|
||||
TDC_OIDC_CLIENT_ID = '0oa27ho9tpZE9Arjy4h7'
|
||||
SOURCE_URL = 'https://source.tandemdiabetes.com/'
|
||||
|
||||
# US Region URLs (default)
|
||||
_US_URLS = {
|
||||
'LOGIN_API_URL': 'https://tdcservices.tandemdiabetes.com/accounts/api/login',
|
||||
'TDC_OAUTH_AUTHORIZE_URL': 'https://tdcservices.tandemdiabetes.com/accounts/api/oauth2/v1/authorize',
|
||||
'TDC_OIDC_JWKS_URL': 'https://tdcservices.tandemdiabetes.com/accounts/api/.well-known/openid-configuration/jwks',
|
||||
'TDC_OIDC_ISSUER': 'https://tdcservices.tandemdiabetes.com/accounts/api',
|
||||
'TDC_OIDC_CLIENT_ID': '0oa27ho9tpZE9Arjy4h7',
|
||||
'SOURCE_URL': 'https://source.tandemdiabetes.com/',
|
||||
'REDIRECT_URI': 'https://sso.tandemdiabetes.com/auth/callback',
|
||||
'TOKEN_ENDPOINT': 'https://tdcservices.tandemdiabetes.com/accounts/api/connect/token',
|
||||
'AUTHORIZATION_ENDPOINT': 'https://tdcservices.tandemdiabetes.com/accounts/api/connect/authorize'
|
||||
}
|
||||
|
||||
# EU Region URLs
|
||||
_EU_URLS = {
|
||||
'LOGIN_API_URL': 'https://tdcservices.eu.tandemdiabetes.com/accounts/api/login',
|
||||
'TDC_OAUTH_AUTHORIZE_URL': 'https://tdcservices.eu.tandemdiabetes.com/accounts/api/oauth2/v1/authorize',
|
||||
'TDC_OIDC_JWKS_URL': 'https://tdcservices.eu.tandemdiabetes.com/accounts/api/.well-known/openid-configuration/jwks',
|
||||
'TDC_OIDC_ISSUER': 'https://tdcservices.eu.tandemdiabetes.com/accounts/api',
|
||||
'TDC_OIDC_CLIENT_ID': '1519e414-eeec-492e-8c5e-97bea4815a10',
|
||||
'SOURCE_URL': 'https://source.eu.tandemdiabetes.com/',
|
||||
'REDIRECT_URI': 'https://source.eu.tandemdiabetes.com/authorize/callback',
|
||||
'TOKEN_ENDPOINT': 'https://tdcservices.eu.tandemdiabetes.com/accounts/api/connect/token',
|
||||
'AUTHORIZATION_ENDPOINT': 'https://tdcservices.eu.tandemdiabetes.com/accounts/api/connect/authorize'
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, email, password):
|
||||
def __init__(self, email, password, region='US'):
|
||||
self.region = region.upper()
|
||||
if self.region not in ['US', 'EU']:
|
||||
raise ValueError(f"Invalid region '{region}'. Must be 'US' or 'EU'.")
|
||||
|
||||
self._region_urls = self._US_URLS if self.region == 'US' else self._EU_URLS
|
||||
|
||||
self.login(email, password)
|
||||
self._email = email
|
||||
self._password = password
|
||||
|
||||
@property
|
||||
def LOGIN_API_URL(self):
|
||||
return self._region_urls['LOGIN_API_URL']
|
||||
|
||||
@property
|
||||
def TDC_OAUTH_AUTHORIZE_URL(self):
|
||||
return self._region_urls['TDC_OAUTH_AUTHORIZE_URL']
|
||||
|
||||
@property
|
||||
def TDC_OIDC_JWKS_URL(self):
|
||||
return self._region_urls['TDC_OIDC_JWKS_URL']
|
||||
|
||||
@property
|
||||
def TDC_OIDC_ISSUER(self):
|
||||
return self._region_urls['TDC_OIDC_ISSUER']
|
||||
|
||||
@property
|
||||
def TDC_OIDC_CLIENT_ID(self):
|
||||
return self._region_urls['TDC_OIDC_CLIENT_ID']
|
||||
|
||||
@property
|
||||
def SOURCE_URL(self):
|
||||
return self._region_urls['SOURCE_URL']
|
||||
|
||||
def login(self, email, password):
|
||||
logger.info("Logging in to TandemSourceApi...")
|
||||
logger.info(f"Logging in to TandemSourceApi ({self.region} region)...")
|
||||
if self.try_load_cached_creds(email):
|
||||
logger.info("Successfully used cached credentials")
|
||||
return True
|
||||
@@ -70,11 +120,10 @@ class TandemSourceApi:
|
||||
|
||||
# oidc
|
||||
client_id = self.TDC_OIDC_CLIENT_ID
|
||||
redirect_uri = 'https://sso.tandemdiabetes.com/auth/callback' # must be an allowlisted URI
|
||||
redirect_uri = self._region_urls['REDIRECT_URI']
|
||||
scope = 'openid profile email'
|
||||
|
||||
token_endpoint = 'https://tdcservices.tandemdiabetes.com/accounts/api/connect/token' #openid_config['token_endpoint']
|
||||
|
||||
token_endpoint = self._region_urls['TOKEN_ENDPOINT']
|
||||
|
||||
def generate_code_verifier():
|
||||
"""Generates a high-entropy code verifier."""
|
||||
@@ -91,7 +140,7 @@ class TandemSourceApi:
|
||||
code_verifier = generate_code_verifier()
|
||||
code_challenge = generate_code_challenge(code_verifier)
|
||||
|
||||
authorization_endpoint = 'https://tdcservices.tandemdiabetes.com/accounts/api/connect/authorize' #openid_config['authorization_endpoint']
|
||||
authorization_endpoint = self._region_urls['AUTHORIZATION_ENDPOINT']
|
||||
|
||||
oidc_step1_params = {
|
||||
'client_id': client_id,
|
||||
@@ -224,6 +273,12 @@ class TandemSourceApi:
|
||||
logger.warning(f"Cached credentials are for a different email ({_saved_blob['cache_creds_email']} in cache, but using {email}), skipping")
|
||||
return False
|
||||
|
||||
# Check if cached region matches current region
|
||||
cached_region = _saved_blob.get('cache_creds_region', 'US') # Default to US for backward compatibility
|
||||
if cached_region != self.region:
|
||||
logger.warning(f"Cached credentials are for a different region ({cached_region} in cache, but using {self.region}), skipping")
|
||||
return False
|
||||
|
||||
at_expiry = _saved_blob['accessTokenExpiresAt']
|
||||
if arrow.get().int_timestamp >= arrow.get(at_expiry).int_timestamp:
|
||||
logger.info(f"Cached credentials have expired ({_saved_blob['accessTokenExpiresAt']}), skipping")
|
||||
@@ -278,6 +333,7 @@ class TandemSourceApi:
|
||||
'cache_creds_version': 1.0,
|
||||
'cache_creds_saved_at': arrow.get(),
|
||||
'cache_creds_email': email,
|
||||
'cache_creds_region': self.region, # Store the region in cache
|
||||
'jwtData': self.jwtData,
|
||||
'pumperId': self.pumperId,
|
||||
'accountId': self.accountId,
|
||||
|
||||
@@ -41,6 +41,7 @@ def get_bool(name, default):
|
||||
|
||||
TCONNECT_EMAIL = get('TCONNECT_EMAIL', 'email@email.com')
|
||||
TCONNECT_PASSWORD = get('TCONNECT_PASSWORD', 'password')
|
||||
TCONNECT_REGION = get_one_of('TCONNECT_REGION', 'US', ['US', 'EU'])
|
||||
|
||||
PUMP_SERIAL_NUMBER = int(get_number('PUMP_SERIAL_NUMBER', '11111111'))
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def fetch_oneshot(username, password, time_start=None, time_end=None):
|
||||
tconnect = TConnectApi(username, password)
|
||||
def fetch_oneshot(username, password, time_start=None, time_end=None, region='US'):
|
||||
tconnect = TConnectApi(username, password, region)
|
||||
if not time_start and not time_end:
|
||||
time_end = datetime.datetime.now()
|
||||
time_start = time_end - datetime.timedelta(days=1)
|
||||
|
||||
@@ -16,5 +16,5 @@ Returns a TConnectApi object with default secret parameters.
|
||||
"""
|
||||
def get_api():
|
||||
from ..api import TConnectApi
|
||||
from ..secret import TCONNECT_EMAIL, TCONNECT_PASSWORD
|
||||
return TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
|
||||
from ..secret import TCONNECT_EMAIL, TCONNECT_PASSWORD, TCONNECT_REGION
|
||||
return TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD, TCONNECT_REGION)
|
||||
Reference in New Issue
Block a user