mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-28 12:11:50 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
822589e4b2 | ||
|
|
90f2406b4f | ||
|
|
9d8554bd03 | ||
|
|
c60b19dbed | ||
|
|
492963e240 | ||
|
|
c9f6362928 | ||
|
|
3d2c2dc280 | ||
|
|
e2b55d55af | ||
|
|
6150619d1a | ||
|
|
7cba6bb0e6 | ||
|
|
d2190db45d |
@@ -42,7 +42,11 @@ jobs:
|
||||
--ignore 61893 \
|
||||
--ignore 61601 \
|
||||
--ignore 62044 \
|
||||
--ignore 67599
|
||||
--ignore 67599 \
|
||||
--ignore 72083 \
|
||||
--ignore 71064 \
|
||||
--ignore 71608 \
|
||||
--ignore 72236
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
@@ -67,4 +71,4 @@ jobs:
|
||||
- name: Upload Coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
fail_ci_if_error: false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 0.9.7
|
||||
version = 1.0.0
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -19,7 +19,8 @@ try:
|
||||
NS_URL,
|
||||
NS_SECRET,
|
||||
NS_SKIP_TLS_VERIFY,
|
||||
PUMP_SERIAL_NUMBER
|
||||
PUMP_SERIAL_NUMBER,
|
||||
NS_IGNORE_CONN_ERRORS
|
||||
)
|
||||
from . import secret
|
||||
except Exception as e:
|
||||
@@ -86,7 +87,7 @@ def main(*args, **kwargs):
|
||||
|
||||
tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
|
||||
|
||||
nightscout = NightscoutApi(NS_URL, NS_SECRET, NS_SKIP_TLS_VERIFY)
|
||||
nightscout = NightscoutApi(NS_URL, NS_SECRET, skip_verify=NS_SKIP_TLS_VERIFY, ignore_conn_errors=NS_IGNORE_CONN_ERRORS)
|
||||
|
||||
if args.check_login:
|
||||
return check_login(tconnect, time_start, time_end)
|
||||
|
||||
@@ -29,6 +29,10 @@ class AndroidApi:
|
||||
ANDROID_API_USERNAME = base64.b64decode('QzIzMzFDRDYtRDQ1MC00OTVFLTlDMTktNjcyMTUyMzBDODVD').decode()
|
||||
ANDROID_API_PASSWORD = base64.b64decode('dHo0MzNLVzVRREM5VjdmIXo2QF4ybyZZNlNHR1lo').decode()
|
||||
|
||||
# These credentials are used by tconnect web
|
||||
TCONNECT_WEB_USERNAME = base64.b64decode('M0U2MzU3QkEtRjYyNS00REQyLUI2NUYtNEI1RTgxNDRBQTZG').decode()
|
||||
TCONNECT_WEB_PASSWORD = base64.b64decode('cUMyaXFIc2w3OFFoR0RYdCpMenFwb1pxZTl3eHN6').decode()
|
||||
|
||||
ANDROID_USER_AGENT = 'Dalvik/2.1.0 (Linux; U; Android 12; Pixel 4a Build/SP2A.220305.012)'
|
||||
|
||||
# These tokens are separate from the "standard" tdcservices API
|
||||
@@ -65,15 +69,17 @@ class AndroidApi:
|
||||
raise ApiLoginException(r.status_code, 'Received HTTP %s during login: %s' % (r.status_code, r.text))
|
||||
|
||||
j = r.json()
|
||||
if "user" not in j or not j["user"]:
|
||||
raise ApiException(r.status_code, 'No user details present in AndroidApi oauth response: %s' % r.text)
|
||||
# tconnect web returns a null user
|
||||
# if "user" not in j or not j["user"]:
|
||||
# raise ApiException(r.status_code, 'No user details present in AndroidApi oauth response: %s' % r.text)
|
||||
|
||||
self.accessToken = j["accessToken"]
|
||||
self.accessTokenExpiresAt = j["accessTokenExpiresAt"]
|
||||
# NOTE: the refresh token is currently unused, instead a new access
|
||||
# token is obtained from scratch by re-logging in when it expires.
|
||||
self.refreshToken = j["refreshToken"]
|
||||
self.refreshTokenExpiresAt = j["refreshTokenExpiresAt"]
|
||||
if "refreshToken" in j and "refreshTokenExpiresAt" in j:
|
||||
self.refreshToken = j["refreshToken"]
|
||||
self.refreshTokenExpiresAt = j["refreshTokenExpiresAt"]
|
||||
self.userId = j["user"]["id"]
|
||||
|
||||
logger.info("Logged in to AndroidApi successfully (expiration: %s, %s)" % (self.accessTokenExpiresAt, timeago(self.accessTokenExpiresAt)))
|
||||
|
||||
@@ -14,7 +14,7 @@ class ControlIQApi:
|
||||
BASE_URL = 'https://tdcservices.tandemdiabetes.com/'
|
||||
LOGIN_URL = 'https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f'
|
||||
|
||||
LAST_CONFIRMED_SOFTWARE_VERSION = 't:connect 7.16.0.1'
|
||||
LAST_CONFIRMED_SOFTWARE_VERSION = 't:connect 7.17.2.3'
|
||||
|
||||
userGuid = None
|
||||
accessToken = None
|
||||
@@ -51,9 +51,21 @@ class ControlIQApi:
|
||||
raise ApiException(fwd.status_code, 'Error retrieving t:connect login cookies.')
|
||||
|
||||
self.userGuid = req.cookies['UserGUID']
|
||||
self.accessToken = req.cookies['accessToken']
|
||||
self.accessTokenExpiresAt = req.cookies['accessTokenExpiresAt']
|
||||
logger.info("Logged in to ControlIQApi successfully (expiration: %s, %s)" % (self.accessTokenExpiresAt, timeago(self.accessTokenExpiresAt)))
|
||||
if 'accessToken' in req.cookies and 'accessTokenExpiresAt' in req.cookies:
|
||||
self.accessToken = req.cookies['accessToken']
|
||||
self.accessTokenExpiresAt = req.cookies['accessTokenExpiresAt']
|
||||
logger.info("Logged in to ControlIQApi successfully via accessToken cookie (expiration: %s, %s)" % (self.accessTokenExpiresAt, timeago(self.accessTokenExpiresAt)))
|
||||
else:
|
||||
logger.info("No accessToken cookie found when logging in to ControlIQApi. Triggering AndroidApi auth")
|
||||
|
||||
from .android import AndroidApi
|
||||
android = AndroidApi(email, password)
|
||||
self.accessToken = android.accessToken
|
||||
self.accessTokenExpiresAt = android.accessTokenExpiresAt
|
||||
|
||||
logger.info("Logged in to AndroidApi successfully via accessToken param (expiration: %s, %s)" % (self.accessTokenExpiresAt, timeago(self.accessTokenExpiresAt)))
|
||||
|
||||
|
||||
self.loginSession = s
|
||||
return True
|
||||
|
||||
@@ -74,6 +86,13 @@ class ControlIQApi:
|
||||
contents = "%s[SNIP]%s" % (contents[:500], contents[-500:])
|
||||
logger.info("BeautifulSoup parsed contents: %s" % contents)
|
||||
pass
|
||||
if not soup.select_one("#__VIEWSTATE"):
|
||||
enc_contents = str(soup.encode_contents())
|
||||
if "Web Page Blocked!" in enc_contents or "Attack ID:" in enc_contents:
|
||||
logger.warn("Being ratelimited/blocked by web application firewall. Sleeping for 30 minutes before retrying.")
|
||||
logger.info("BeautifulSoup parsed contents: %s" % enc_contents)
|
||||
time.sleep(1800)
|
||||
exit(1)
|
||||
return {
|
||||
"__LASTFOCUS": "",
|
||||
"__EVENTTARGET": "ctl00$ContentBody$LoginControl$linkLogin",
|
||||
@@ -96,6 +115,9 @@ class ControlIQApi:
|
||||
return None
|
||||
|
||||
def needs_relogin(self):
|
||||
if not self.accessTokenExpiresAt:
|
||||
return False
|
||||
|
||||
diff = (arrow.get(self.accessTokenExpiresAt) - arrow.get())
|
||||
return (diff.seconds <= 5 * 60)
|
||||
|
||||
|
||||
+43
-24
@@ -30,10 +30,11 @@ def time_range(field_name, start_time, end_time, t_to_space=False):
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
class NightscoutApi:
|
||||
def __init__(self, url, secret, skip_verify=False):
|
||||
def __init__(self, url, secret, skip_verify=False, ignore_conn_errors=False):
|
||||
self.url = url
|
||||
self.secret = secret
|
||||
self.verify = False if skip_verify else None
|
||||
self.ignore_conn_errors = ignore_conn_errors
|
||||
|
||||
|
||||
def upload_entry(self, ns_format, entity='treatments'):
|
||||
@@ -76,22 +77,27 @@ class NightscoutApi:
|
||||
if j and len(j) > 0:
|
||||
return j[0]
|
||||
return None
|
||||
|
||||
ret = None
|
||||
try:
|
||||
ret = internal(False)
|
||||
except ApiException as e:
|
||||
logger.warning("last_uploaded_entry with no t_to_space: %s", e)
|
||||
ret = None
|
||||
if ret is None and (time_start or time_end):
|
||||
try:
|
||||
ret = internal(True)
|
||||
ret = internal(False)
|
||||
except ApiException as e:
|
||||
logger.warning("last_uploaded_entry with t_to_space: %s", e)
|
||||
logger.warning("last_uploaded_entry with no t_to_space: %s", e)
|
||||
ret = None
|
||||
if ret is not None:
|
||||
logger.warning("last_uploaded_entry with eventType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (eventType, time_start, time_end))
|
||||
return ret
|
||||
if ret is None and (time_start or time_end):
|
||||
try:
|
||||
ret = internal(True)
|
||||
except ApiException as e:
|
||||
logger.warning("last_uploaded_entry with t_to_space: %s", e)
|
||||
ret = None
|
||||
if ret is not None:
|
||||
logger.warning("last_uploaded_entry with eventType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (eventType, time_start, time_end))
|
||||
return ret
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
if self.ignore_conn_errors:
|
||||
logger.warn('Ignoring ConnectionError because ignore_conn_errors=true', e)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def last_uploaded_bg_entry(self, time_start=None, time_end=None):
|
||||
def internal(t_to_space):
|
||||
@@ -107,12 +113,19 @@ class NightscoutApi:
|
||||
return j[0]
|
||||
return None
|
||||
|
||||
ret = internal(False)
|
||||
if ret is None and (time_start or time_end):
|
||||
ret = internal(True)
|
||||
if ret is not None:
|
||||
logger.warning("last_uploaded_bg_entry with time_start=%s time_end=%s only returned data when timestamps contained a space" % (time_start, time_end))
|
||||
return ret
|
||||
try:
|
||||
ret = internal(False)
|
||||
if ret is None and (time_start or time_end):
|
||||
ret = internal(True)
|
||||
if ret is not None:
|
||||
logger.warning("last_uploaded_bg_entry with time_start=%s time_end=%s only returned data when timestamps contained a space" % (time_start, time_end))
|
||||
|
||||
return ret
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
if self.ignore_conn_errors:
|
||||
logger.warn('Ignoring ConnectionError because ignore_conn_errors=true', e)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def last_uploaded_activity(self, activityType, time_start=None, time_end=None):
|
||||
def internal(t_to_space):
|
||||
@@ -128,12 +141,18 @@ class NightscoutApi:
|
||||
return j[0]
|
||||
return None
|
||||
|
||||
ret = internal(False)
|
||||
if ret is None and (time_start or time_end):
|
||||
ret = internal(True)
|
||||
if ret is not None:
|
||||
logger.warning("last_uploaded_activity with activityType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (activityType, time_start, time_end))
|
||||
return ret
|
||||
try:
|
||||
ret = internal(False)
|
||||
if ret is None and (time_start or time_end):
|
||||
ret = internal(True)
|
||||
if ret is not None:
|
||||
logger.warning("last_uploaded_activity with activityType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (activityType, time_start, time_end))
|
||||
return ret
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
if self.ignore_conn_errors:
|
||||
logger.warn('Ignoring ConnectionError because ignore_conn_errors=true', e)
|
||||
else:
|
||||
raise e
|
||||
|
||||
"""
|
||||
Returns general status information about the Nightscout server.
|
||||
|
||||
@@ -49,6 +49,7 @@ if not get('NS_SECRET') and get('API_SECRET'):
|
||||
NS_SECRET = get('API_SECRET')
|
||||
|
||||
NS_SKIP_TLS_VERIFY = get_bool('NS_SKIP_TLS_VERIFY', 'false')
|
||||
NS_IGNORE_CONN_ERRORS = get_bool('NS_IGNORE_CONN_ERRORS', 'false')
|
||||
|
||||
TIMEZONE_NAME = get('TIMEZONE_NAME', 'America/New_York')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user