From a18c2f3d17c972dbeb0e12b30db677a2fbbe748d Mon Sep 17 00:00:00 2001 From: James Woglom Date: Tue, 9 Aug 2022 22:54:51 -0400 Subject: [PATCH] move Bolus internal state from dictionary to domain object --- tconnectsync/domain/__init__.py | 0 tconnectsync/domain/bolus.py | 18 ++++++++++++ tconnectsync/parser/ciq_therapy_events.py | 16 ++++++++++ tconnectsync/parser/tconnect.py | 24 ++++++++++++--- tconnectsync/process.py | 36 +++++++++++++++++++---- tconnectsync/sync/bolus.py | 34 ++++++++++----------- tests/parser/test_tconnect.py | 21 ++++++------- tests/sync/test_bolus.py | 6 ++-- 8 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 tconnectsync/domain/__init__.py create mode 100644 tconnectsync/domain/bolus.py create mode 100644 tconnectsync/parser/ciq_therapy_events.py diff --git a/tconnectsync/domain/__init__.py b/tconnectsync/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tconnectsync/domain/bolus.py b/tconnectsync/domain/bolus.py new file mode 100644 index 0000000..1df0867 --- /dev/null +++ b/tconnectsync/domain/bolus.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + + +@dataclass +class Bolus: + description: str + complete: str # "1" / "0" + completion: str + request_time: str # _datetime_parse timestamp + completion_time: str # _datetime_parse timestamp + insulin: str + requested_insulin: str + carbs: str + bg: str # potentially "" + user_override: str + extended_bolus: str # "1" / "0" + bolex_completion_time: str + bolex_start_time: str diff --git a/tconnectsync/parser/ciq_therapy_events.py b/tconnectsync/parser/ciq_therapy_events.py new file mode 100644 index 0000000..9bc7deb --- /dev/null +++ b/tconnectsync/parser/ciq_therapy_events.py @@ -0,0 +1,16 @@ +from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent +from tconnectsync.parser.tconnect import TConnectEntry + + +def split_therapy_events(ciqTherapyEvents): + bolusEvents = [] + cgmEvents = [] + for e in ciqTherapyEvents['event']: + event = TConnectEntry.parse_therapy_event(e) + if type(event) == BolusTherapyEvent: + bolusEvents.append(event) + elif type(event) == CGMTherapyEvent: + cgmEvents.append(event) + + + return bolusEvents, cgmEvents diff --git a/tconnectsync/parser/tconnect.py b/tconnectsync/parser/tconnect.py index 45ca9bc..7a34898 100644 --- a/tconnectsync/parser/tconnect.py +++ b/tconnectsync/parser/tconnect.py @@ -1,6 +1,9 @@ from os import stat import sys import arrow +from tconnectsync.domain.bolus import Bolus + +from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent try: from ..secret import TIMEZONE_NAME @@ -105,7 +108,7 @@ class TConnectEntry: complete = is_complete(data["ExtendedBolusIsComplete"]) or is_complete(data["BolusIsComplete"]) extended_bolus = ("extended" in data["Description"].lower()) - return { + return Bolus(**{ "description": data["Description"], "complete": "1" if complete else "", "completion": data["CompletionStatusDesc"] if not extended_bolus else data["BolexCompletionStatusDesc"], @@ -120,7 +123,7 @@ class TConnectEntry: # Note: completion time can be empty if the extended bolus is in progress "bolex_completion_time": TConnectEntry._datetime_parse(data["BolexCompletionDateTime"]).format() if data["BolexCompletionDateTime"] and complete and extended_bolus else None, "bolex_start_time": TConnectEntry._datetime_parse(data["BolexStartDateTime"]).format() if data["BolexStartDateTime"] and complete and extended_bolus else None, - } + }) @staticmethod def parse_reading_entry(data): @@ -189,7 +192,16 @@ class TConnectEntry: "time": time.format(), "event_type": TConnectEntry.BASALSUSPENSION_EVENTS[data["SuspendReason"]] } - + + # Parses an entry from controliq.therapy_events() and returns a TherapyEvent + @staticmethod + def parse_therapy_event(data): + if data["type"] == "Bolus": + return BolusTherapyEvent.parse(data) + elif data["type"] == "CGM": + return CGMTherapyEvent.parse(data) + + raise UnknownTherapyEventException(data) class UnknownCIQActivityEventException(Exception): def __init__(self, data): @@ -197,4 +209,8 @@ class UnknownCIQActivityEventException(Exception): class UnknownBasalSuspensionEventException(Exception): def __init__(self, data): - super().__init__("Unknown basal suspension event type: %s" % data) \ No newline at end of file + super().__init__("Unknown basal suspension event type: %s" % data) + +class UnknownTherapyEventException(Exception): + def __init__(self, data): + super().__init__("Unknown therapy event type: " % data) diff --git a/tconnectsync/process.py b/tconnectsync/process.py index 3038226..e479a3b 100644 --- a/tconnectsync/process.py +++ b/tconnectsync/process.py @@ -3,6 +3,8 @@ import datetime import arrow import time +from tconnectsync.parser.ciq_therapy_events import split_therapy_events + from .util import timeago from .api.common import ApiException from .sync.basal import ( @@ -56,7 +58,24 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat csvBasalData = None csvBolusData = None - if CGM in features or BOLUS in features or BOLUS_BG in features or IOB in features: + ciqBolusData = None + ciqReadingData = None + if BOLUS in features: + logger.info("Downloading t:connect therapy_events") + ciqTherapyEventsData = tconnect.controliq.therapy_events(time_start, time_end) + ciqBolusData, ciqReadingData = split_therapy_events(ciqTherapyEventsData) + + if ciqReadingData and len(ciqReadingData) > 0: + lastReading = ciqReadingData[-1].eventDateTime + lastReading = TConnectEntry._datetime_parse(lastReading) + logger.debug(ciqReadingData[-1]) + logger.info("Last CGM reading from t:connect CIQ: %s (%s)" % (lastReading, timeago(lastReading))) + else: + logger.warning("No last CGM reading is able to be determined from CIQ") + + if (BOLUS in features and not ciqBolusData) or \ + (CGM in features and not ciqReadingData) or \ + BOLUS_BG in features or IOB in features: logger.info("Downloading t:connect CSV data") csvdata = tconnect.ws2.therapy_timeline_csv(time_start, time_end) @@ -69,9 +88,10 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat lastReading = csvReadingData[-1]['EventDateTime'] if 'EventDateTime' in csvReadingData[-1] else 0 lastReading = TConnectEntry._datetime_parse(lastReading) logger.debug(csvReadingData[-1]) - logger.info("Last CGM reading from t:connect: %s (%s)" % (lastReading, timeago(lastReading))) + logger.info("Last CGM reading from t:connect CSV: %s (%s)" % (lastReading, timeago(lastReading))) else: - logger.warning("No last CGM reading is able to be determined") + logger.warning("No last CGM reading is able to be determined from CSV") + added = 0 @@ -108,10 +128,14 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat added += ns_write_pump_events(nightscout, pumpEvents, pretend=pretend, time_start=time_start, time_end=time_end) - if csvBolusData: - if BOLUS in features: + if BOLUS in features: + bolusEvents = [] + if ciqBolusData: + bolusEvents = process_bolus_therapy_events(ciqBolusData) + + if csvBolusData and not bolusEvents: bolusEvents = process_bolus_events(csvBolusData) - added += ns_write_bolus_events(nightscout, bolusEvents, pretend=pretend, include_bg=(BOLUS_BG in features), time_start=time_start, time_end=time_end) + added += ns_write_bolus_events(nightscout, bolusEvents, pretend=pretend, include_bg=(BOLUS_BG in features), time_start=time_start, time_end=time_end) if csvIobData: if IOB in features: diff --git a/tconnectsync/sync/bolus.py b/tconnectsync/sync/bolus.py index e4d65d1..39c2a76 100644 --- a/tconnectsync/sync/bolus.py +++ b/tconnectsync/sync/bolus.py @@ -20,21 +20,21 @@ def process_bolus_events(bolusdata, cgmEvents=None): for b in bolusdata: parsed = TConnectEntry.parse_bolus_entry(b) - if parsed["completion"] != "Completed": - if parsed["insulin"] and float(parsed["insulin"]) > 0: + if parsed.completion != "Completed": + if parsed.insulin and float(parsed.insulin) > 0: # Count non-completed bolus if any insulin was delivered (vs. the amount of insulin requested) - parsed["description"] += " (%s: requested %s units)" % (parsed["completion"], parsed["requested_insulin"]) + parsed.description += " (%s: requested %s units)" % (parsed.completion, parsed.requested_insulin) else: logger.warning("Skipping non-completed bolus data (was a bolus in progress?): %s parsed: %s" % (b, parsed)) continue - if parsed["bg"] and cgmEvents: - requested_at = parsed["request_time"] if not parsed["extended_bolus"] else parsed["bolex_start_time"] - parsed["bg_type"] = guess_bolus_bg_type(parsed["bg"], requested_at, cgmEvents) + if parsed.bg and cgmEvents: + requested_at = parsed.request_time if not parsed.extended_bolus else parsed.bolex_start_time + parsed.bg_type = guess_bolus_bg_type(parsed.bg, requested_at, cgmEvents) bolusEvents.append(parsed) - bolusEvents.sort(key=lambda event: arrow.get(event["request_time"] if not event["extended_bolus"] else event["bolex_start_time"])) + bolusEvents.sort(key=lambda event: arrow.get(event.request_time if not event.extended_bolus else event.bolex_start_time)) return bolusEvents @@ -72,27 +72,27 @@ def ns_write_bolus_events(nightscout, bolusEvents, pretend=False, include_bg=Fal add_count = 0 for event in bolusEvents: - created_at = event["completion_time"] if not event["extended_bolus"] else event["bolex_start_time"] + created_at = event.completion_time if not event.extended_bolus else event.bolex_start_time if last_upload_time and arrow.get(created_at) <= last_upload_time: if pretend: logger.info("Skipping basal event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end)) continue - if include_bg and event["bg"]: + if include_bg and event.bg: entry = NightscoutEntry.bolus( - bolus=event["insulin"], - carbs=event["carbs"], + bolus=event.insulin, + carbs=event.carbs, created_at=created_at, - notes="{}{}{}".format(event["description"], " (Override)" if event["user_override"] == "1" else "", " (Extended)" if event["extended_bolus"] == "1" else ""), - bg=event["bg"], - bg_type=event["bg_type"] + notes="{}{}{}".format(event.description, " (Override)" if event.user_override == "1" else "", " (Extended)" if event.extended_bolus == "1" else ""), + bg=event.bg, + bg_type=event.bg_type ) else: entry = NightscoutEntry.bolus( - bolus=event["insulin"], - carbs=event["carbs"], + bolus=event.insulin, + carbs=event.carbs, created_at=created_at, - notes="{}{}{}".format(event["description"], " (Override)" if event["user_override"] == "1" else "", " (Extended)" if event["extended_bolus"] == "1" else "") + notes="{}{}{}".format(event.description, " (Override)" if event.user_override == "1" else "", " (Extended)" if event.extended_bolus == "1" else "") ) add_count += 1 diff --git a/tests/parser/test_tconnect.py b/tests/parser/test_tconnect.py index bcb8fc1..195a2f9 100644 --- a/tests/parser/test_tconnect.py +++ b/tests/parser/test_tconnect.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import unittest +from tconnectsync.domain.bolus import Bolus from tconnectsync.parser.tconnect import TConnectEntry, UnknownBasalSuspensionEventException, UnknownCIQActivityEventException class TestTConnectEntryBasal(unittest.TestCase): @@ -177,7 +178,7 @@ class TestTConnectEntryBolus(unittest.TestCase): def test_parse_bolus_entry_std_correction(self): self.assertEqual( TConnectEntry.parse_bolus_entry(self.entryStdCorrection), - { + Bolus(**{ "description": "Standard/Correction", "complete": "1", "completion": "Completed", @@ -191,7 +192,7 @@ class TestTConnectEntryBolus(unittest.TestCase): "extended_bolus": "", "bolex_completion_time": None, "bolex_start_time": None - }) + })) entryStd = { "Type": "Bolus", @@ -239,7 +240,7 @@ class TestTConnectEntryBolus(unittest.TestCase): def test_parse_bolus_entry_std(self): self.assertEqual( TConnectEntry.parse_bolus_entry(self.entryStd), - { + Bolus(**{ "description": "Standard", "complete": "1", "completion": "Completed", @@ -253,7 +254,7 @@ class TestTConnectEntryBolus(unittest.TestCase): "extended_bolus": "", "bolex_completion_time": None, "bolex_start_time": None - }) + })) entryStdAutomatic = { "Type": "Bolus", @@ -301,7 +302,7 @@ class TestTConnectEntryBolus(unittest.TestCase): def test_parse_bolus_entry_std_automatic(self): self.assertEqual( TConnectEntry.parse_bolus_entry(self.entryStdAutomatic), - { + Bolus(**{ "description": "Automatic Bolus/Correction", "complete": "1", "completion": "Completed", @@ -315,7 +316,7 @@ class TestTConnectEntryBolus(unittest.TestCase): "extended_bolus": "", "bolex_completion_time": None, "bolex_start_time": None - }) + })) entryStdIncompleteZero = { "Type": "Bolus", @@ -363,7 +364,7 @@ class TestTConnectEntryBolus(unittest.TestCase): def test_parse_bolus_entry_std_incomplete_zero(self): self.assertEqual( TConnectEntry.parse_bolus_entry(self.entryStdIncompleteZero), - { + Bolus(**{ "description": "Standard", "complete": "", "completion": "User Aborted", @@ -377,7 +378,7 @@ class TestTConnectEntryBolus(unittest.TestCase): "extended_bolus": "", "bolex_completion_time": None, "bolex_start_time": None - }) + })) entryStdIncompletePartial = { "Type": "Bolus", @@ -425,7 +426,7 @@ class TestTConnectEntryBolus(unittest.TestCase): def test_parse_bolus_entry_std_incomplete_partial(self): self.assertEqual( TConnectEntry.parse_bolus_entry(self.entryStdIncompletePartial), - { + Bolus(**{ "description": "Standard/Correction", "complete": "", "completion": "Terminated by Alarm", @@ -439,7 +440,7 @@ class TestTConnectEntryBolus(unittest.TestCase): "extended_bolus": "", "bolex_completion_time": None, "bolex_start_time": None - }) + })) class TestTConnectEntryReading(unittest.TestCase): entry1 = { diff --git a/tests/sync/test_bolus.py b/tests/sync/test_bolus.py index 8f26580..a4d175e 100644 --- a/tests/sync/test_bolus.py +++ b/tests/sync/test_bolus.py @@ -52,7 +52,7 @@ class TestBolusSync(unittest.TestCase): self.assertEqual(len(bolusEvents), len(bolusData)) def set_bg_type(entry, type): - entry["bg_type"] = type + entry.bg_type = type return entry # Expect FINGER for bolus entries with a BG because there's no matching event with the same BG @@ -111,7 +111,7 @@ class TestBolusSync(unittest.TestCase): self.assertEqual(len(bolusEvents), len(bolusData)) def set_bg_type(entry, type): - entry["bg_type"] = type + entry.bg_type = type return entry expected = [ @@ -145,7 +145,7 @@ class TestBolusSync(unittest.TestCase): ] for e in partialEntries: - e["description"] += " (%s: requested %s units)" % (e["completion"], e["requested_insulin"]) + e.description += " (%s: requested %s units)" % (e.completion, e.requested_insulin) self.assertListEqual(bolusEvents, [ TConnectEntry.parse_bolus_entry(d) for d in stdData