mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
process tconnect basal entries
This commit is contained in:
+1
-1
@@ -3,4 +3,4 @@ __pycache__
|
||||
*.pyo
|
||||
*.swp
|
||||
*.swa
|
||||
|
||||
secret.py
|
||||
@@ -8,6 +8,8 @@ verify_ssl = true
|
||||
[packages]
|
||||
requests = "*"
|
||||
bs4 = "*"
|
||||
arrow = "*"
|
||||
lxml = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
Generated
+60
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "dd4ac29a565a87289ea1d1f0daa29d2f7310a1f5799f2877e58d2603e689b706"
|
||||
"sha256": "9a07b247ba95f523a90ebd0d8530416e24afbb20cc62fd265b09c7361b941232"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,6 +16,14 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"arrow": {
|
||||
"hashes": [
|
||||
"sha256:92aac856ea5175c804f7ccb96aca4d714d936f1c867ba59d747a8096ec30e90a",
|
||||
"sha256:98184d8dd3e5d30b96c2df4596526f7de679ccb467f358b82b0f686436f3a6b8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"beautifulsoup4": {
|
||||
"hashes": [
|
||||
"sha256:73cc4d115b96f79c7d77c1c7f7a0a8d4c57860d1041df407dd1aae7f07a77fd7",
|
||||
@@ -52,6 +60,50 @@
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f",
|
||||
"sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730",
|
||||
"sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f",
|
||||
"sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1",
|
||||
"sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3",
|
||||
"sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7",
|
||||
"sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a",
|
||||
"sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe",
|
||||
"sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1",
|
||||
"sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e",
|
||||
"sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d",
|
||||
"sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20",
|
||||
"sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae",
|
||||
"sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5",
|
||||
"sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba",
|
||||
"sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293",
|
||||
"sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a",
|
||||
"sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6",
|
||||
"sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88",
|
||||
"sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed",
|
||||
"sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843",
|
||||
"sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443",
|
||||
"sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0",
|
||||
"sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304",
|
||||
"sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258",
|
||||
"sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6",
|
||||
"sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1",
|
||||
"sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481",
|
||||
"sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef",
|
||||
"sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd",
|
||||
"sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.5.2"
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
@@ -60,6 +112,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"soupsieve": {
|
||||
"hashes": [
|
||||
"sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55",
|
||||
|
||||
@@ -2,16 +2,23 @@ import requests
|
||||
import json
|
||||
import urllib
|
||||
import datetime
|
||||
import csv
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
class TConnectApi:
|
||||
API_BASE_URL = 'https://tdcservices.tandemdiabetes.com/tconnect/controliq/api/'
|
||||
CONTROLIQ_BASE_URL = 'https://tdcservices.tandemdiabetes.com/tconnect/controliq/api/'
|
||||
LOGIN_URL = 'https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f'
|
||||
|
||||
THERAPYTIMELINE2CSV_URL = 'https://tconnectws2.tandemdiabetes.com/therapytimeline2csv/%s/%s/%s'
|
||||
|
||||
userGuid = None
|
||||
accessToken = None
|
||||
accessTokenExpiresAt = None
|
||||
|
||||
def __init__(self, email, password):
|
||||
if not self.login(email, password):
|
||||
raise Exception('Unable to authenticate')
|
||||
|
||||
def login(self, email, password):
|
||||
with requests.Session() as s:
|
||||
initial = s.get(self.LOGIN_URL)
|
||||
@@ -46,16 +53,54 @@ class TConnectApi:
|
||||
raise Exception('No access token provided')
|
||||
return {'Authorization': 'Bearer %s' % self.accessToken}
|
||||
|
||||
def api(self, endpoint, query):
|
||||
r = requests.get(self.API_BASE_URL + endpoint, query, headers=self.api_headers())
|
||||
def controliq_api(self, endpoint, query):
|
||||
r = requests.get(self.CONTROLIQ_BASE_URL + endpoint, query, headers=self.api_headers())
|
||||
if r.status_code != 200:
|
||||
print("API response:", r.status_code, r.text)
|
||||
return r.json()
|
||||
|
||||
def _parse_date(self, date):
|
||||
if type(date) == str:
|
||||
return date
|
||||
return (date or datetime.datetime.now()).strftime('%m-%d-%Y')
|
||||
|
||||
def therapy_timeline(self, start=None, end=None):
|
||||
startDate = start if type(start) == str else (start or datetime.datetime.now()).strftime('%m-%d-%Y')
|
||||
endDate = end if type(end) == str else (end or datetime.datetime.now()).strftime('%m-%d-%Y')
|
||||
startDate = self._parse_date(start)
|
||||
endDate = self._parse_date(end)
|
||||
|
||||
return self.api('therapytimeline/users/%s' % (self.userGuid), {
|
||||
return self.controliq_api('therapytimeline/users/%s' % (self.userGuid), {
|
||||
"startDate": startDate,
|
||||
"endDate": endDate
|
||||
})
|
||||
|
||||
def _split_empty_sections(self, text):
|
||||
sections = [[]]
|
||||
sectionIndex = 0
|
||||
for line in text.splitlines():
|
||||
if len(line.strip()) > 0:
|
||||
sections[sectionIndex].append(line)
|
||||
else:
|
||||
sections.append([])
|
||||
sectionIndex += 1
|
||||
|
||||
return sections + [None] * (4 - len(sections))
|
||||
|
||||
def _csv_to_dict(self, rawdata):
|
||||
data = []
|
||||
headers = rawdata[0].split(",")
|
||||
for row in csv.reader(rawdata[1:]):
|
||||
data.append({headers[i]: row[i] for i in range(len(row)) if i < len(headers)})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def therapy_timeline_csv(self, start=None, end=None):
|
||||
startDate = self._parse_date(start)
|
||||
endDate = self._parse_date(end)
|
||||
|
||||
r = requests.get(self.THERAPYTIMELINE2CSV_URL % (self.userGuid, startDate, endDate))
|
||||
|
||||
header, readingData, iobData, bolusData = self._split_empty_sections(r.text)
|
||||
|
||||
return self._csv_to_dict(readingData), self._csv_to_dict(iobData), self._csv_to_dict(bolusData)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import sys
|
||||
import requests
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
from secret import NS_URL, NS_SECRET, TIMEZONE_NAME
|
||||
except Exception:
|
||||
print('Unable to import Nightscout secrets from secret.py')
|
||||
sys.exit(1)
|
||||
|
||||
ENTERED_BY = "Tandem Pump (tconnectsync)"
|
||||
|
||||
class NightscoutEntry:
|
||||
@staticmethod
|
||||
def basal(value, duration_mins, created_at, reason=""):
|
||||
return {
|
||||
"eventType": "Temp Basal",
|
||||
"reason": reason,
|
||||
"duration": int(round(duration_mins)),
|
||||
"absolute": float(value),
|
||||
"created_at": created_at,
|
||||
"carbs": None,
|
||||
"insulin": None,
|
||||
"enteredBy": ENTERED_BY
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def bolus(bolus, carbs, created_at, notes=""):
|
||||
return {
|
||||
"eventType": "Meal Bolus",
|
||||
"created_at": created_at,
|
||||
"carbs": carbs,
|
||||
"insulin": bolus,
|
||||
"notes": notes,
|
||||
"enteredBy": ENTERED_BY,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def upload_nightscout(ns_format):
|
||||
upload = requests.post(NS_URL + 'api/v1/treatments?api_secret=' + NS_SECRET, json=ns_format, headers={
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'api-secret': hashlib.sha1(NS_SECRET.encode()).hexdigest()
|
||||
})
|
||||
print("Nightscout upload status:", upload.status_code, upload.text)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import sys
|
||||
import arrow
|
||||
|
||||
try:
|
||||
from secret import TIMEZONE_NAME
|
||||
except Exception:
|
||||
print('Unable to import parser secrets from secret.py')
|
||||
sys.exit(1)
|
||||
|
||||
class TConnectEntry:
|
||||
@staticmethod
|
||||
def _epoch_parse(x):
|
||||
# data["x"] is an integer epoch timestamp which, when read as an equivalent timestamp
|
||||
# stored in Pacific time (America/Los_Angeles), contains the user's local time, but
|
||||
# with the wrong timezone data attached.
|
||||
#
|
||||
# For example, data["x"] references UTC timestamp 2020-09-01T13:00:00+00:00,
|
||||
# which when read in Pacific time is equivalent to 2020-09-01T06:00:00-07:00.
|
||||
# However, the user's timezone is Eastern time, so the timezone of America/Los_Angeles
|
||||
# is overwritten with America/New_York, resulting in 2020-09-01T06:00:00-04:00, the
|
||||
# correct timestamp.
|
||||
return arrow.get(x, tzinfo="America/Los_Angeles").replace(tzinfo=TIMEZONE_NAME)
|
||||
|
||||
@staticmethod
|
||||
def parse_basal_entry(data, delivery_type=""):
|
||||
time = TConnectEntry._epoch_parse(data["x"])
|
||||
duration_mins = data["duration"] / 60
|
||||
basal_rate = data["y"]
|
||||
|
||||
return {
|
||||
"time": time.format(),
|
||||
"delivery_type": delivery_type,
|
||||
"duration_mins": duration_mins,
|
||||
"basal_rate": basal_rate,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_suspension_entry(data):
|
||||
time = TConnectEntry._epoch_parse(data["x"])
|
||||
return {
|
||||
"time": time.format(),
|
||||
"continuation": data["continuation"],
|
||||
"suspendReason": data["suspendReason"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _datetime_parse(date):
|
||||
return arrow.get(date, tzinfo=TIMEZONE_NAME)
|
||||
|
||||
@staticmethod
|
||||
def parse_cgm_entry(data):
|
||||
# EventDateTime is stored in the user's timezone.
|
||||
return {
|
||||
"time": TConnectEntry._datetime_parse(data["EventDateTime"]).format(),
|
||||
"reading": data["Readings (CGM / BGM)"],
|
||||
"reading_type": data["Description"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_iob_entry(data):
|
||||
# EventDateTime is stored in the user's timezone.
|
||||
return {
|
||||
"time": TConnectEntry._datetime_parse(data["EventDateTime"]).format(),
|
||||
"iob": data["IOB"],
|
||||
"event_id": data["EventID"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_bolus_entry(data):
|
||||
# RequestDateTime and CompletionDateTime are stored in the user's timezone.
|
||||
return {
|
||||
"description": data["Description"],
|
||||
"completion": data["CompletionStatusDesc"],
|
||||
"request_time": TConnectEntry._datetime_parse(data["RequestDateTime"]).format(),
|
||||
"completion_time": TConnectEntry._datetime_parse(data["CompletionDateTime"]).format(),
|
||||
"insulin": data["InsulinDelivered"],
|
||||
"carbs": data["CarbSize"],
|
||||
"user_override": data["UserOverride"],
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import datetime
|
||||
import json
|
||||
import hashlib
|
||||
import requests
|
||||
import arrow
|
||||
|
||||
from api import TConnectApi
|
||||
from parser import TConnectEntry
|
||||
from nightscout import NightscoutEntry, upload_nightscout
|
||||
|
||||
try:
|
||||
from secret import TCONNECT_EMAIL, TCONNECT_PASSWORD
|
||||
except Exception:
|
||||
print('Unable to import secret.py')
|
||||
sys.exit(1)
|
||||
|
||||
def process_basal_events(data):
|
||||
suspensionEvents = {}
|
||||
for s in data["suspensionDeliveryEvents"]:
|
||||
entry = TConnectEntry.parse_suspension_entry(s)
|
||||
suspensionEvents[entry["time"]] = entry
|
||||
|
||||
basalEvents = []
|
||||
for b in data["basal"]["tempDeliveryEvents"]:
|
||||
basalEvents.append(TConnectEntry.parse_basal_entry(b, delivery_type="tempDelivery"))
|
||||
|
||||
for b in data["basal"]["algorithmDeliveryEvents"]:
|
||||
basalEvents.append(TConnectEntry.parse_basal_entry(b, delivery_type="algorithmDelivery"))
|
||||
|
||||
for b in data["basal"]["profileDeliveryEvents"]:
|
||||
basalEvents.append(TConnectEntry.parse_basal_entry(b, delivery_type="profileDelivery"))
|
||||
|
||||
basalEvents.sort(key=lambda x: arrow.get(x["time"]))
|
||||
|
||||
for i in basalEvents:
|
||||
print(i)
|
||||
if i["time"] in suspensionEvents:
|
||||
i["suspendReason"] = suspensionEvents[i["time"]]["suspendReason"]
|
||||
|
||||
return basalEvents
|
||||
|
||||
def main():
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
|
||||
# data = tconnect.therapy_timeline(now - datetime.timedelta(days=1), now)
|
||||
# open("basaldata.json","w").write(json.dumps(data))
|
||||
|
||||
data = json.loads(open("basaldata.json").read())
|
||||
basalEvents = process_basal_events(data)
|
||||
|
||||
#csvdata = tconnect.therapy_timeline_csv()
|
||||
#open("csvdata.json", "w").write(json.dumps(csvdata))
|
||||
# csvdata = json.loads(open("csvdata.json").read())
|
||||
|
||||
# for b in csvdata[1]:
|
||||
# print(TConnectEntry.parse_iob_entry(b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user