diff --git a/Pipfile b/Pipfile index 03d2f15..d1a79ae 100644 --- a/Pipfile +++ b/Pipfile @@ -11,5 +11,3 @@ bs4 = "*" arrow = "*" lxml = "*" -[requires] -python_version = "3.7" diff --git a/api.py b/api.py index f873c9e..70a7ae8 100644 --- a/api.py +++ b/api.py @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..ad823bb --- /dev/null +++ b/main.py @@ -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() \ No newline at end of file diff --git a/nightscout.py b/nightscout.py index 15a8216..a20733f 100644 --- a/nightscout.py +++ b/nightscout.py @@ -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 \ No newline at end of file diff --git a/parser.py b/parser.py index fdd5276..e1c26b3 100644 --- a/parser.py +++ b/parser.py @@ -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. diff --git a/sync.py b/sync.py deleted file mode 100644 index 203b666..0000000 --- a/sync.py +++ /dev/null @@ -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() \ No newline at end of file