mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-28 12:11:50 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe57bd6c14 | ||
|
|
0872c756d2 | ||
|
|
7d8dabb3ef | ||
|
|
798225c606 | ||
|
|
b08b9b8c76 | ||
|
|
90bbd5dc63 | ||
|
|
5e56b41b62 | ||
|
|
e8fa6c5a7c | ||
|
|
fae1d4de2c |
@@ -1,9 +1,9 @@
|
||||
name: Publish Docker image
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
@@ -21,3 +21,35 @@ jobs:
|
||||
registry: docker.pkg.github.com
|
||||
repository: jwoglom/tconnectsync/tconnectsync
|
||||
tag_with_ref: true
|
||||
|
||||
push_to_dockerhub:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
with:
|
||||
images: jwoglom/tconnectsync
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 0.6.5
|
||||
version = 0.7.1
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
from .android import AndroidApi
|
||||
from .controliq import ControlIQApi
|
||||
from .ws2 import WS2Api
|
||||
from .webui import WebUIScraper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +18,7 @@ class TConnectApi:
|
||||
self._ciq = None
|
||||
self._ws2 = None
|
||||
self._android = None
|
||||
self._webui = None
|
||||
|
||||
|
||||
@property
|
||||
@@ -52,4 +54,14 @@ class TConnectApi:
|
||||
|
||||
self._android = AndroidApi(self.email, self.password)
|
||||
return self._android
|
||||
|
||||
@property
|
||||
def webui(self):
|
||||
if self._webui and not self._webui.needs_relogin():
|
||||
return self._webui
|
||||
|
||||
logger.debug("Instantiating new WebUIScraper")
|
||||
|
||||
self._webui = WebUIScraper(self.controliq)
|
||||
return self._webui
|
||||
|
||||
|
||||
+14
-15
@@ -29,6 +29,8 @@ class AndroidApi:
|
||||
ANDROID_API_USERNAME = base64.b64decode('QzIzMzFDRDYtRDQ1MC00OTVFLTlDMTktNjcyMTUyMzBDODVD').decode()
|
||||
ANDROID_API_PASSWORD = base64.b64decode('dHo0MzNLVzVRREM5VjdmIXo2QF4ybyZZNlNHR1lo').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
|
||||
accessToken = None
|
||||
accessTokenExpiresAt = None
|
||||
@@ -51,7 +53,10 @@ class AndroidApi:
|
||||
'grant_type': 'password',
|
||||
'scope': self.OAUTH_SCOPES
|
||||
},
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
headers={
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'User-Agent': self.ANDROID_USER_AGENT
|
||||
},
|
||||
auth=requests.auth.HTTPBasicAuth(self.ANDROID_API_USERNAME, self.ANDROID_API_PASSWORD)
|
||||
)
|
||||
|
||||
@@ -82,7 +87,11 @@ class AndroidApi:
|
||||
return {'Authorization': 'Bearer %s' % self.accessToken}
|
||||
|
||||
def _get(self, endpoint, query={}, **kwargs):
|
||||
r = requests.get(self.BASE_URL + endpoint, query, headers=self.api_headers(), **kwargs)
|
||||
r = requests.get(self.BASE_URL + endpoint, query, headers={
|
||||
'User-Agent': self.ANDROID_USER_AGENT,
|
||||
'Content-Type': 'application/json',
|
||||
**self.api_headers()
|
||||
}, **kwargs)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise ApiException(r.status_code, "Android API HTTP %s response: %s" % (str(r.status_code), r.text))
|
||||
@@ -134,6 +143,9 @@ class AndroidApi:
|
||||
# TODO: these methods are used in the web app, not the Android app,
|
||||
# but support the same auth tokens and are on this domain. They should
|
||||
# be moved to a new Api class.
|
||||
# 3/17/2022: the API appears to be more stringently checking scopes,
|
||||
# and some of these endpoints no longer work with the API token scoped
|
||||
# to the Android app.
|
||||
|
||||
"""
|
||||
Returns BG and pump threshold values.
|
||||
@@ -154,16 +166,3 @@ class AndroidApi:
|
||||
"""
|
||||
def user_profile(self):
|
||||
return self.get('cloud/usersettings/api/UserProfile?userId=%s' % self.userId)
|
||||
|
||||
"""
|
||||
Returns therapy events, used by the webui Therapy Timeline.
|
||||
{'event': [
|
||||
{'type': 'Basal', 'basalRate': ...},
|
||||
{'type': 'Bolus', 'standard': ...},
|
||||
{'type': 'CGM', 'egv': ...}
|
||||
]}
|
||||
"""
|
||||
def therapy_events(self, start_date=None, end_date=None):
|
||||
startDate = parse_date(start_date)
|
||||
endDate = parse_date(end_date)
|
||||
return self.get('tconnect/therapyevents/api/TherapyEvents/%s/%s/false?userId=%s' % (startDate, endDate, self.userId))
|
||||
|
||||
@@ -13,7 +13,7 @@ from .common import parse_date, base_headers, ApiException, ApiLoginException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ControlIQApi:
|
||||
BASE_URL = 'https://tdcservices.tandemdiabetes.com/tconnect/controliq/api/'
|
||||
BASE_URL = 'https://tdcservices.tandemdiabetes.com/'
|
||||
LOGIN_URL = 'https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f'
|
||||
|
||||
userGuid = None
|
||||
@@ -45,6 +45,7 @@ class ControlIQApi:
|
||||
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)))
|
||||
self.loginSession = s
|
||||
return True
|
||||
|
||||
def _build_login_data(self, email, password, soup):
|
||||
@@ -106,7 +107,7 @@ class ControlIQApi:
|
||||
startDate = parse_date(start)
|
||||
endDate = parse_date(end)
|
||||
|
||||
return self.get('therapytimeline/users/%s' % (self.userGuid), {
|
||||
return self.get('tconnect/controliq/api/therapytimeline/users/%s' % (self.userGuid), {
|
||||
"startDate": startDate,
|
||||
"endDate": endDate
|
||||
})
|
||||
@@ -122,7 +123,7 @@ class ControlIQApi:
|
||||
startDate = parse_date(start)
|
||||
endDate = parse_date(end)
|
||||
|
||||
return self.get('summary/users/%s' % (self.userGuid), {
|
||||
return self.get('tconnect/controliq/api/summary/users/%s' % (self.userGuid), {
|
||||
"startDate": startDate,
|
||||
"endDate": endDate
|
||||
})
|
||||
@@ -132,4 +133,17 @@ class ControlIQApi:
|
||||
[{"serialNumber": "11111111", "features": {"controlIQ": {"feature": 1, "dateTimeFirstDetected": "YYYY-MM-DD:THH:MM:SS", "unixTimestamp": 1111111111}}}]
|
||||
"""
|
||||
def pumpfeatures(self):
|
||||
return self.get('pumpfeatures/users/%s' % self.userGuid, {})
|
||||
return self.get('tconnect/controliq/api/pumpfeatures/users/%s' % self.userGuid, {})
|
||||
|
||||
"""
|
||||
Returns therapy events, used by the webui Therapy Timeline.
|
||||
{'event': [
|
||||
{'type': 'Basal', 'basalRate': ...},
|
||||
{'type': 'Bolus', 'standard': ...},
|
||||
{'type': 'CGM', 'egv': ...}
|
||||
]}
|
||||
"""
|
||||
def therapy_events(self, start_date=None, end_date=None):
|
||||
startDate = parse_date(start_date)
|
||||
endDate = parse_date(end_date)
|
||||
return self.get('tconnect/therapyevents/api/TherapyEvents/%s/%s/false?userId=%s' % (startDate, endDate, self.userGuid), {})
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import requests
|
||||
import urllib
|
||||
import datetime
|
||||
import arrow
|
||||
import time
|
||||
import logging
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .common import base_headers, ApiException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
WebUIScraper contains data that is scraped from the t:connect Web UI and is
|
||||
not accessible via any known API.
|
||||
"""
|
||||
class WebUIScraper:
|
||||
BASE_URL = "https://tconnect.tandemdiabetes.com/"
|
||||
|
||||
def __init__(self, controliq):
|
||||
self.controliq = controliq
|
||||
|
||||
def needs_relogin(self):
|
||||
return self.controliq.needs_relogin()
|
||||
|
||||
def _get(self, endpoint):
|
||||
r = self.controliq.loginSession.get(self.BASE_URL + endpoint, headers=base_headers())
|
||||
|
||||
if r.status_code != 200:
|
||||
raise ApiException(r.status_code, "WebUIScraper HTTP %s response: %s" % (str(r.status_code), r.text))
|
||||
return r
|
||||
|
||||
|
||||
def get(self, endpoint, tries=0):
|
||||
try:
|
||||
return self._get(endpoint)
|
||||
except ApiException as e:
|
||||
logger.warning("Received ApiException in WebUIScraper with endpoint '%s' (tries %d): %s" % (endpoint, tries, e))
|
||||
if tries > 0:
|
||||
raise ApiException(e.status_code, "WebUIScraper HTTP %d on retry #%d: %s", e.status_code, tries, e)
|
||||
|
||||
# Trigger automatic re-login, and try again once
|
||||
if e.status_code == 401:
|
||||
logger.info("Performing automatic re-login to ControlIQApi after HTTP 401 for ControlIQApi")
|
||||
self.controliq.accessTokenExpiresAt = time.time()
|
||||
self.controliq.login(self.controliq._email, self.controliq._password)
|
||||
|
||||
return self.get(endpoint, tries=tries+1)
|
||||
|
||||
if e.status_code == 500:
|
||||
return self.get(endpoint, tries=tries+1)
|
||||
|
||||
raise e
|
||||
|
||||
def strip(self, txt):
|
||||
# Remove errant whitespace between litearl newlines (and literal )
|
||||
sep = '\r\n'
|
||||
if sep not in txt and '\n' in txt:
|
||||
sep = '\n'
|
||||
return ' '.join([i.replace('\xa0',' ').strip() for i in txt.strip().split(sep)])
|
||||
|
||||
|
||||
"""
|
||||
Returns a mapping between pump/device IDs and information about that device,
|
||||
including the GUID used for obtaining pump settings.
|
||||
"""
|
||||
def my_devices(self):
|
||||
devices = {}
|
||||
r = self.get('myaccount/my_devices.aspx')
|
||||
soup = BeautifulSoup(r.content, features='lxml')
|
||||
|
||||
for device in soup.select('#content > div.box'):
|
||||
device_name = self.strip(device.select_one('.subTitle').text)
|
||||
|
||||
def find_label_value(lbl):
|
||||
label = device.find(text=lbl)
|
||||
if label:
|
||||
tds = label.parent.parent.parent.select('td')
|
||||
if len(tds) > 1:
|
||||
return self.strip(tds[1].text)
|
||||
return None
|
||||
|
||||
serial_number = find_label_value('Serial #')
|
||||
model_number = find_label_value('Model #')
|
||||
status = find_label_value('Status')
|
||||
|
||||
settings_span = device.find(text='View Settings')
|
||||
settings_guid = None
|
||||
if settings_span:
|
||||
settings_a = settings_span.parent.parent
|
||||
settings_guid = settings_a.attrs['href'].split('?guid=')[1]
|
||||
|
||||
if serial_number:
|
||||
devices[serial_number] = {
|
||||
'name': device_name,
|
||||
'model_number': model_number,
|
||||
'status': status,
|
||||
'guid': settings_guid
|
||||
}
|
||||
|
||||
return devices
|
||||
|
||||
"""
|
||||
Returns a parsed representation of a pump's settings.
|
||||
Note that pump_guid is NOT the serial number of the pump, and
|
||||
should be obtained from my_devices()[str(serial_number)]['guid']
|
||||
"""
|
||||
def device_settings_from_guid(self, pump_guid):
|
||||
profiles = []
|
||||
settings = {}
|
||||
r = self.get('myaccount/DeviceSettings.aspx?guid=%s' % pump_guid)
|
||||
soup = BeautifulSoup(r.content, features='lxml')
|
||||
settings["upload_date"] = self.strip(soup.select_one('#lblUploadDate').text)
|
||||
|
||||
divxml = soup.select_one('#divXML')
|
||||
divxmlDiv = divxml.findChild('div')
|
||||
for tbl in divxmlDiv.findChildren('table', recursive=False):
|
||||
setting_bg = tbl.select_one('.setting_bg')
|
||||
if setting_bg and self.strip(setting_bg.text) == 'Profile':
|
||||
profiles.append(self._parse_profile_tbl(tbl))
|
||||
else:
|
||||
settings.update(self._parse_settings_tbl(tbl))
|
||||
|
||||
return profiles, settings
|
||||
|
||||
def _parse_profile_tbl(self, tbl):
|
||||
profile = {}
|
||||
profile["title"] = self.strip(tbl.select_one('.setting_title').text)
|
||||
profile["active"] = bool(tbl.find(text='Active at the time of upload'))
|
||||
profile["segments"] = []
|
||||
|
||||
for tr in tbl.select('tr'):
|
||||
# Skip header rows
|
||||
if tr.select_one('.setting_bg'):
|
||||
continue
|
||||
if tr.find(text='Start Time'):
|
||||
continue
|
||||
|
||||
tds = tr.select('td')
|
||||
def is_time_row(td):
|
||||
txt = self.strip(td.select_one('strong').text)
|
||||
return " AM" in txt or " PM" in txt or txt in ("Midnight", "Noon")
|
||||
|
||||
if len(tds) > 0 and is_time_row(tds[0]):
|
||||
display_time = self.strip(tds[0].text)
|
||||
t = display_time
|
||||
if display_time == "Midnight":
|
||||
t = "12:00 AM"
|
||||
elif display_time == "Noon":
|
||||
t = "12:00 PM"
|
||||
segment = {
|
||||
"display_time": display_time,
|
||||
"time": t,
|
||||
"basal_rate": self.strip(tds[1].text),
|
||||
"correction_factor": self.strip(tds[2].text),
|
||||
"carb_ratio": self.strip(tds[3].text),
|
||||
"target_bg": self.strip(tds[4].text)
|
||||
}
|
||||
profile["segments"].append(segment)
|
||||
continue
|
||||
|
||||
if tr.find(text='Calculated Total Daily Basal'):
|
||||
profile["calculated_total_daily_basal"] = self.strip(tds[1].text)
|
||||
continue
|
||||
|
||||
# Last row
|
||||
if tr.find(text='Duration of Insulin:'):
|
||||
lastrow = self.strip(tr.text)
|
||||
for part in lastrow.split(' |'):
|
||||
if len(part) < 1:
|
||||
continue
|
||||
|
||||
key, val = part.split(': ')
|
||||
key = self.strip(key)
|
||||
val = self.strip(val)
|
||||
if key == 'Duration of Insulin':
|
||||
profile["insulin_duration"] = val
|
||||
elif key == 'Carbohydrates':
|
||||
profile["carbohydrates"] = val
|
||||
|
||||
|
||||
return profile
|
||||
|
||||
def _parse_settings_tbl(self, tbl):
|
||||
outer_tr = tbl.select('tr')[2]
|
||||
|
||||
settings = {}
|
||||
def loop(td, subhead):
|
||||
settings[subhead] = {}
|
||||
for tr in td.select('.settingstable > tr'):
|
||||
if not tr.select_one('strong'):
|
||||
continue
|
||||
|
||||
key = self.strip(tr.select_one('strong').text)
|
||||
|
||||
tds = tr.select('td')
|
||||
if len(tds) == 1:
|
||||
subhead = key
|
||||
settings[subhead] = {}
|
||||
continue
|
||||
|
||||
val_text = self.strip(tds[1].text)
|
||||
val = {}
|
||||
if tds[1].find(text=' - '):
|
||||
val['value'] = False
|
||||
elif tds[1].find(text='Off'):
|
||||
val['value'] = False
|
||||
val_text = self.strip(val_text.split('Off', 1)[1])
|
||||
elif tds[1].find(text='On'):
|
||||
val['value'] = True
|
||||
val_text = self.strip(val_text.split('On', 1)[1])
|
||||
|
||||
val['text'] = val_text
|
||||
settings[subhead][key] = val
|
||||
|
||||
children = outer_tr.findChildren('td', recursive=False)
|
||||
loop(children[0], 'Alerts')
|
||||
loop(children[1], 'Pump Settings')
|
||||
|
||||
return settings
|
||||
|
||||
"""
|
||||
Wraps a call to my_devices to identify the device GUID from the
|
||||
given pump serial, and then returns device_settings_from_guid.
|
||||
"""
|
||||
def device_settings(self, pump_serial):
|
||||
devices = self.my_devices()
|
||||
if str(pump_serial) in devices.keys():
|
||||
dev = devices[str(pump_serial)]
|
||||
return self.device_settings_from_guid(dev['guid'])
|
||||
|
||||
raise RuntimeError('Unable to find pump with serial number: %s. Known devices: %s' % (pump_serial, devices))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+79
-22
@@ -1,10 +1,15 @@
|
||||
import sys
|
||||
import time
|
||||
import arrow
|
||||
import logging
|
||||
import pkg_resources
|
||||
from datetime import datetime
|
||||
from pprint import pformat
|
||||
|
||||
from .nightscout import NightscoutApi
|
||||
from .parser.nightscout import BASAL_EVENTTYPE, BOLUS_EVENTTYPE
|
||||
from .parser.tconnect import TConnectEntry
|
||||
from .sync.basal import process_ciq_basal_events
|
||||
|
||||
try:
|
||||
__version__ = pkg_resources.require("tconnectsync")[0].version
|
||||
@@ -16,7 +21,7 @@ Attempts to authenticate with each t:connect API,
|
||||
and returns the output of a sample API call from each.
|
||||
Also attempts to connect to the Nightscout API.
|
||||
"""
|
||||
def check_login(tconnect, time_start, time_end, verbose=False):
|
||||
def check_login(tconnect, time_start, time_end, verbose=False, sanitize=False):
|
||||
errors = 0
|
||||
|
||||
loglines = []
|
||||
@@ -70,36 +75,58 @@ def check_login(tconnect, time_start, time_end, verbose=False):
|
||||
log("Logging in to t:connect ControlIQ API...")
|
||||
try:
|
||||
summary = tconnect.controliq.dashboard_summary(time_start, time_end)
|
||||
debug("ControlIQ dashboard summary: %s" % summary)
|
||||
debug("ControlIQ dashboard summary: \n%s" % pformat(summary))
|
||||
except Exception as e:
|
||||
log("Error occurred querying ControlIQ API:")
|
||||
log("Error occurred querying ControlIQ API for dashboard_summary:")
|
||||
log(e)
|
||||
errors += 1
|
||||
|
||||
log("Querying ControlIQ therapy_timeline...")
|
||||
lastBasalTime = None
|
||||
lastBasalDuration = None
|
||||
try:
|
||||
tt = tconnect.controliq.therapy_timeline(time_start, time_end)
|
||||
debug("ControlIQ therapy_timeline: %s" % tt)
|
||||
debug("ControlIQ therapy_timeline: \n%s" % pformat(tt))
|
||||
if tt:
|
||||
processed_tt = process_ciq_basal_events(tt)
|
||||
debug("ControlIQ processed therapy_timeline: \n%s" % pformat(processed_tt))
|
||||
if processed_tt:
|
||||
log("Last ControlIQ processed therapy_timeline event: \n%s" % pformat(processed_tt[-1]))
|
||||
lastBasalTime = processed_tt[-1]['time']
|
||||
lastBasalDuration = processed_tt[-1]['duration_mins']
|
||||
except Exception as e:
|
||||
log("Error occurred querying ControlIQ therapy_timeline:")
|
||||
log(e)
|
||||
errors += 1
|
||||
|
||||
log("Querying ControlIQ therapy_events...")
|
||||
try:
|
||||
androidevents = tconnect.controliq.therapy_events(time_start, time_end)
|
||||
debug("controliq therapy_events: \n%s" % pformat(androidevents))
|
||||
except Exception as e:
|
||||
log("Error occurred querying ControlIQ therapy_events:")
|
||||
log(e)
|
||||
errors += 1
|
||||
|
||||
log("-----")
|
||||
|
||||
log("Logging in to t:connect WS2 API...")
|
||||
try:
|
||||
summary = tconnect.ws2.basaliqtech(time_start, time_end)
|
||||
debug("WS2 basaliq status: %s" % summary)
|
||||
debug("WS2 basaliq status: \n%s" % pformat(summary))
|
||||
except Exception as e:
|
||||
log("Error occurred querying WS2 API:")
|
||||
log(e)
|
||||
errors += 1
|
||||
|
||||
log("Querying WS2 therapy_timeline_csv...")
|
||||
lastReadingTime = None
|
||||
try:
|
||||
ttcsv = tconnect.ws2.therapy_timeline_csv(time_start, time_end)
|
||||
debug("therapy_timeline_csv: %s", ttcsv)
|
||||
debug("therapy_timeline_csv: \n%s" % pformat(ttcsv))
|
||||
if ttcsv and "readingData" in ttcsv and len(ttcsv["readingData"]) > 0:
|
||||
log("Last therapy_timeline_csv reading: \n%s" % pformat(ttcsv["readingData"][-1]))
|
||||
lastReadingTime = TConnectEntry._datetime_parse(ttcsv["readingData"][-1]['EventDateTime'])
|
||||
except Exception as e:
|
||||
log("Error occurred querying WS2 therapy_timeline_csv:")
|
||||
log(e)
|
||||
@@ -108,38 +135,31 @@ def check_login(tconnect, time_start, time_end, verbose=False):
|
||||
log("-----")
|
||||
|
||||
log("Logging in to t:connect Android API...")
|
||||
summary = None
|
||||
try:
|
||||
summary = tconnect.android.user_profile()
|
||||
debug("Android user profile: %s" % summary)
|
||||
debug("Android user profile: \n%s" % pformat(summary))
|
||||
|
||||
event = tconnect.android.last_event_uploaded(PUMP_SERIAL_NUMBER)
|
||||
debug("Android last uploaded event: %s" % event)
|
||||
debug("Android last uploaded event: \n%s" % pformat(event))
|
||||
except Exception as e:
|
||||
log("Error occurred querying Android API:")
|
||||
log(e)
|
||||
errors += 1
|
||||
|
||||
log("Querying Android therapy_events...")
|
||||
try:
|
||||
androidevents = tconnect.android.therapy_events(time_start, time_end)
|
||||
debug("android therapy_events: %s" % androidevents)
|
||||
except Exception as e:
|
||||
log("Error occurred querying Android therapy_events:")
|
||||
log(e)
|
||||
errors += 1
|
||||
|
||||
log("-----")
|
||||
|
||||
log("Logging in to Nightscout...")
|
||||
try:
|
||||
nightscout = NightscoutApi(NS_URL, NS_SECRET)
|
||||
status = nightscout.api_status()
|
||||
debug("Nightscout status: %s" % status)
|
||||
debug("Nightscout status: \n%s" % pformat(status))
|
||||
|
||||
last_upload_basal = nightscout.last_uploaded_entry(BASAL_EVENTTYPE)
|
||||
debug("Nightscout last uploaded basal: %s" % last_upload_basal)
|
||||
debug("Nightscout last uploaded basal: \n%s" % pformat(last_upload_basal))
|
||||
|
||||
last_upload_bolus = nightscout.last_uploaded_entry(BOLUS_EVENTTYPE)
|
||||
debug("Nightscout last uploaded bolus: %s" % last_upload_bolus)
|
||||
debug("Nightscout last uploaded bolus: \n%s" % pformat(last_upload_bolus))
|
||||
except Exception as e:
|
||||
log("Error occurred querying Nightscout API:")
|
||||
log(e)
|
||||
@@ -147,6 +167,15 @@ def check_login(tconnect, time_start, time_end, verbose=False):
|
||||
|
||||
log("-----")
|
||||
|
||||
def time_ago(t):
|
||||
return '%s ago' % (arrow.now() - arrow.get(t)) if t else 'n/a'
|
||||
|
||||
log("Last basal start time: %s (%s)" % (lastBasalTime, time_ago(lastBasalTime)))
|
||||
log("Last basal duration: %s" % lastBasalDuration)
|
||||
log("Last reading time: %s (%s)" % (lastReadingTime, time_ago(lastReadingTime)))
|
||||
|
||||
log("-----")
|
||||
|
||||
if errors == 0:
|
||||
log("No API errors returned!")
|
||||
else:
|
||||
@@ -154,9 +183,37 @@ def check_login(tconnect, time_start, time_end, verbose=False):
|
||||
|
||||
|
||||
with open('tconnectsync-check-output.log', 'w') as f:
|
||||
|
||||
if sanitize:
|
||||
sanitizedData = {
|
||||
'TCONNECT_EMAIL': TCONNECT_EMAIL,
|
||||
'TCONNECT_PASSWORD': TCONNECT_PASSWORD,
|
||||
'PUMP_SERIAL_NUMBER': PUMP_SERIAL_NUMBER,
|
||||
'NS_URL': NS_URL,
|
||||
'NS_SECRET': NS_SECRET
|
||||
}
|
||||
|
||||
if summary:
|
||||
sanitizedData.update({
|
||||
'ANDROID_PROFILE_USERID': summary.get('userID'),
|
||||
'ANDROID_PROFILE_PATIENT_FULLNAME': summary.get('patientFullName'),
|
||||
'ANDROID_PROFILE_CAREGIVER_FULLNAME': summary.get('caregiverFullName')
|
||||
})
|
||||
|
||||
loglines = [run_sanitize(i, sanitizedData) for i in loglines]
|
||||
|
||||
f.writelines(loglines)
|
||||
|
||||
print("Created file tconnectsync-check-output.log containing additional debugging information.")
|
||||
print("For support, you can upload this file to https://github.com/jwoglom/tconnectsync/issues/new")
|
||||
print("Before uploading, look through the file and remove any sensitive data, such as")
|
||||
print("Nightscout URL and pump serial number.")
|
||||
if sanitize:
|
||||
print("The file -- but NOT the output printed above -- has been sanitized to remove sensitive data.")
|
||||
print("Please verify and remove any sensitive data, such as your Nightscout URL/secret and pump serial number,")
|
||||
print("as necessary.")
|
||||
|
||||
def run_sanitize(s, sanitizedData):
|
||||
ret = str(s)
|
||||
for k, v in sanitizedData.items():
|
||||
if v and len(str(v)) > 0:
|
||||
ret = ret.replace(str(v), '[%s]' % k)
|
||||
return ret
|
||||
@@ -38,6 +38,10 @@ class AndroidApi(tconnectsync.api.android.AndroidApi):
|
||||
def _get(self, endpoint, query={}, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
class WebUIScraper(tconnectsync.api.webui.WebUIScraper):
|
||||
def __init__(self, controliq):
|
||||
self.controliq = controliq
|
||||
|
||||
class TConnectApi(tconnectsync.api.TConnectApi):
|
||||
def __init__(self, email=None, password=None):
|
||||
if email is not None and password is not None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user