mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
upload parsed tconnect bolus, basal, and iob entries to nightscout
This commit is contained in:
@@ -11,5 +11,3 @@ bs4 = "*"
|
||||
arrow = "*"
|
||||
lxml = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
@@ -58,7 +58,7 @@ class TConnectApi:
|
||||
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
|
||||
@@ -72,7 +72,7 @@ class TConnectApi:
|
||||
"startDate": startDate,
|
||||
"endDate": endDate
|
||||
})
|
||||
|
||||
|
||||
def _split_empty_sections(self, text):
|
||||
sections = [[]]
|
||||
sectionIndex = 0
|
||||
@@ -84,13 +84,15 @@ class TConnectApi:
|
||||
sectionIndex += 1
|
||||
|
||||
return sections + [None] * (4 - len(sections))
|
||||
|
||||
|
||||
def _csv_to_dict(self, rawdata):
|
||||
data = []
|
||||
if not rawdata or len(rawdata) == 0:
|
||||
return 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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/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,
|
||||
delete_nightscout,
|
||||
last_uploaded_nightscout_entry,
|
||||
last_uploaded_nightscout_activity,
|
||||
BASAL_EVENTTYPE,
|
||||
BOLUS_EVENTTYPE,
|
||||
IOB_ACTIVITYTYPE
|
||||
)
|
||||
|
||||
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 ns_write_basal_events(basalEvents):
|
||||
last_upload = last_uploaded_nightscout_entry(BASAL_EVENTTYPE)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
print("Last Nightscout basal upload:", last_upload_time)
|
||||
|
||||
for event in basalEvents:
|
||||
if last_upload_time and arrow.get(event["time"]) <= last_upload_time:
|
||||
#print("Skipping already uploaded basal event:", event)
|
||||
continue
|
||||
|
||||
entry = NightscoutEntry.basal(
|
||||
value=event["basal_rate"],
|
||||
duration_mins=event["duration_mins"],
|
||||
created_at=event["time"],
|
||||
reason=event["delivery_type"]
|
||||
)
|
||||
print("Processing basal:", event, "entry:", entry)
|
||||
upload_nightscout(entry)
|
||||
|
||||
def process_bolus_events(bolusdata):
|
||||
bolusEvents = []
|
||||
|
||||
for b in bolusdata:
|
||||
parsed = TConnectEntry.parse_bolus_entry(b)
|
||||
if parsed["completion"] != "Completed":
|
||||
print("Skipping non-completed bolus data:", b, "parsed:", parsed)
|
||||
continue
|
||||
bolusEvents.append(parsed)
|
||||
|
||||
bolusEvents.sort(key=lambda x: arrow.get(x["completion_time"]))
|
||||
|
||||
return bolusEvents
|
||||
|
||||
def ns_write_bolus_events(bolusEvents):
|
||||
last_upload = last_uploaded_nightscout_entry(BOLUS_EVENTTYPE)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
print("Last Nightscout bolus upload:", last_upload_time)
|
||||
|
||||
for event in bolusEvents:
|
||||
if last_upload_time and arrow.get(event["completion_time"]) <= last_upload_time:
|
||||
#print("Skipping already uploaded bolus event:", event)
|
||||
continue
|
||||
|
||||
entry = NightscoutEntry.bolus(
|
||||
bolus=event["insulin"],
|
||||
carbs=event["carbs"],
|
||||
created_at=event["completion_time"],
|
||||
notes="{}{}".format(event["description"], " (Override)" if event["user_override"] == "1" else "")
|
||||
)
|
||||
|
||||
print("Processing bolus:", event, "entry:", entry)
|
||||
upload_nightscout(entry)
|
||||
|
||||
def process_iob_events(iobdata):
|
||||
iobEvents = []
|
||||
for d in iobdata:
|
||||
iobEvents.append(TConnectEntry.parse_iob_entry(d))
|
||||
|
||||
iobEvents.sort(key=lambda x: arrow.get(x["time"]))
|
||||
|
||||
return iobEvents
|
||||
|
||||
def ns_write_iob_events(iobEvents):
|
||||
last_upload = last_uploaded_nightscout_activity(IOB_ACTIVITYTYPE)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
print("Last Nightscout iob upload:", last_upload_time)
|
||||
|
||||
event = iobEvents[-1]
|
||||
if last_upload_time and arrow.get(event["time"]) <= last_upload_time:
|
||||
print("Skipping already uploaded iob event:", event)
|
||||
return
|
||||
|
||||
entry = NightscoutEntry.iob(
|
||||
iob=event["iob"],
|
||||
created_at=event["time"]
|
||||
)
|
||||
|
||||
print("Processing iob:", event, "entry:", entry)
|
||||
upload_nightscout(entry, entity='activity')
|
||||
|
||||
# Delete the previous activity
|
||||
if last_upload and '_id' in last_upload:
|
||||
print("Deleting old iob entry:", last_upload)
|
||||
delete_nightscout('activity/{}'.format(last_upload['_id']))
|
||||
|
||||
def main():
|
||||
now = datetime.datetime.now()
|
||||
|
||||
tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
|
||||
|
||||
basaldata = tconnect.therapy_timeline(now - datetime.timedelta(days=1), now)
|
||||
|
||||
# open("basaldata.json","w").write(json.dumps(data))
|
||||
# basaldata = json.loads(open("basaldata.json").read())
|
||||
|
||||
basalEvents = process_basal_events(basaldata)
|
||||
ns_write_basal_events(basalEvents)
|
||||
|
||||
csvdata = tconnect.therapy_timeline_csv(now - datetime.timedelta(days=1), now)
|
||||
cgmdata, iobdata, bolusdata = csvdata
|
||||
|
||||
print("Most recent cgmdata:", cgmdata[-1])
|
||||
|
||||
# open("csvdata.json", "w").write(json.dumps(csvdata))
|
||||
# cgmdata, iobdata, bolusdata = json.loads(open("csvdata.json").read())
|
||||
|
||||
bolusEvents = process_bolus_events(bolusdata)
|
||||
ns_write_bolus_events(bolusEvents)
|
||||
|
||||
iobEvents = process_iob_events(iobdata)
|
||||
ns_write_iob_events(iobEvents)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+44
-7
@@ -1,6 +1,8 @@
|
||||
import sys
|
||||
import requests
|
||||
import hashlib
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
try:
|
||||
from secret import NS_URL, NS_SECRET, TIMEZONE_NAME
|
||||
@@ -8,13 +10,16 @@ except Exception:
|
||||
print('Unable to import Nightscout secrets from secret.py')
|
||||
sys.exit(1)
|
||||
|
||||
ENTERED_BY = "Tandem Pump (tconnectsync)"
|
||||
ENTERED_BY = "Pump (tconnectsync)"
|
||||
BASAL_EVENTTYPE = "Temp Basal"
|
||||
BOLUS_EVENTTYPE = "Combo Bolus"
|
||||
IOB_ACTIVITYTYPE = "tconnect_iob"
|
||||
|
||||
class NightscoutEntry:
|
||||
@staticmethod
|
||||
def basal(value, duration_mins, created_at, reason=""):
|
||||
return {
|
||||
"eventType": "Temp Basal",
|
||||
"eventType": BASAL_EVENTTYPE,
|
||||
"reason": reason,
|
||||
"duration": int(round(duration_mins)),
|
||||
"absolute": float(value),
|
||||
@@ -23,11 +28,11 @@ class NightscoutEntry:
|
||||
"insulin": None,
|
||||
"enteredBy": ENTERED_BY
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def bolus(bolus, carbs, created_at, notes=""):
|
||||
return {
|
||||
"eventType": "Meal Bolus",
|
||||
"eventType": BOLUS_EVENTTYPE,
|
||||
"created_at": created_at,
|
||||
"carbs": carbs,
|
||||
"insulin": bolus,
|
||||
@@ -35,13 +40,45 @@ class NightscoutEntry:
|
||||
"enteredBy": ENTERED_BY,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def iob(iob, created_at):
|
||||
return {
|
||||
"activityType": IOB_ACTIVITYTYPE,
|
||||
"iob": iob,
|
||||
"created_at": created_at,
|
||||
"enteredBy": ENTERED_BY
|
||||
}
|
||||
|
||||
|
||||
def upload_nightscout(ns_format):
|
||||
upload = requests.post(NS_URL + 'api/v1/treatments?api_secret=' + NS_SECRET, json=ns_format, headers={
|
||||
def upload_nightscout(ns_format, entity='treatments'):
|
||||
upload = requests.post(NS_URL + 'api/v1/' + entity + '?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)
|
||||
|
||||
def delete_nightscout(entity):
|
||||
upload = requests.delete(NS_URL + 'api/v1/' + entity + '?api_secret=' + NS_SECRET, json={}, headers={
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'api-secret': hashlib.sha1(NS_SECRET.encode()).hexdigest()
|
||||
})
|
||||
print("Nightscout delete status:", upload.status_code, upload.text)
|
||||
|
||||
def last_uploaded_nightscout_entry(eventType):
|
||||
latest = requests.get(NS_URL + 'api/v1/treatments?count=1&find[enteredBy]=' + urllib.parse.quote(ENTERED_BY) + '&find[eventType]=' + urllib.parse.quote(eventType) + '&ts=' + str(time.time()), headers={
|
||||
'api-secret': hashlib.sha1(NS_SECRET.encode()).hexdigest()
|
||||
})
|
||||
j = latest.json()
|
||||
if j and len(j) > 0:
|
||||
return j[0]
|
||||
return None
|
||||
|
||||
def last_uploaded_nightscout_activity(activityType):
|
||||
latest = requests.get(NS_URL + 'api/v1/activity?find[enteredBy]=' + urllib.parse.quote(ENTERED_BY) + '&find[activityType]=' + urllib.parse.quote(activityType) + '&ts=' + str(time.time()), headers={
|
||||
'api-secret': hashlib.sha1(NS_SECRET.encode()).hexdigest()
|
||||
})
|
||||
j = latest.json()
|
||||
if j and len(j) > 0:
|
||||
return j[0]
|
||||
return None
|
||||
@@ -33,7 +33,7 @@ class TConnectEntry:
|
||||
"duration_mins": duration_mins,
|
||||
"basal_rate": basal_rate,
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_suspension_entry(data):
|
||||
time = TConnectEntry._epoch_parse(data["x"])
|
||||
@@ -42,7 +42,7 @@ class TConnectEntry:
|
||||
"continuation": data["continuation"],
|
||||
"suspendReason": data["suspendReason"],
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _datetime_parse(date):
|
||||
return arrow.get(date, tzinfo=TIMEZONE_NAME)
|
||||
@@ -55,7 +55,7 @@ class TConnectEntry:
|
||||
"reading": data["Readings (CGM / BGM)"],
|
||||
"reading_type": data["Description"],
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_iob_entry(data):
|
||||
# EventDateTime is stored in the user's timezone.
|
||||
@@ -64,7 +64,7 @@ class TConnectEntry:
|
||||
"iob": data["IOB"],
|
||||
"event_id": data["EventID"],
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_bolus_entry(data):
|
||||
# RequestDateTime and CompletionDateTime are stored in the user's timezone.
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/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