internally track bolus from CIQ API identically at par with WS2

This commit is contained in:
James Woglom
2022-08-10 01:31:45 -04:00
parent f13929a45a
commit 2f5775cbfb
7 changed files with 95 additions and 18 deletions
+8 -1
View File
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, asdict
@dataclass
@@ -16,3 +16,10 @@ class Bolus:
extended_bolus: str # "1" / "0"
bolex_completion_time: str
bolex_start_time: str
def to_dict(self):
return asdict(self)
@property
def is_extended_bolus(self):
return self.extended_bolus == "1"
+5 -3
View File
@@ -6,7 +6,9 @@ from ..secret import TIMEZONE_NAME
def _datetime_parse(date):
return arrow.get(date, tzinfo=TIMEZONE_NAME).format()
# consistent format with ws2 endpoint
return arrow.get(date, tzinfo=TIMEZONE_NAME).format("YYYY-MM-DD HH:mm:ssZZ")
class TherapyEvent:
type = None
eventDateTime = None
@@ -96,7 +98,7 @@ class BolusTherapyEvent(TherapyEvent):
self.bg = json.get("bg")
self.user_override = json.get("userOverride")
self.extended_bolus = json.get("bolusRequestOptions") == "Extended"
if self.extended_bolus:
if self.extended_bolus and self.complete:
# TODO(https://github.com/jwoglom/tconnectsync/issues/19): read more extended bolus info
self.complete = json.get("bolex", {}).get("extendedBolusIsComplete")
self.completion = json.get("bolex", {}).get("completionStatusDesc")
@@ -116,7 +118,7 @@ class BolusTherapyEvent(TherapyEvent):
completion_time=_datetime_parse(self.completion_time),
insulin=str(self.insulin),
requested_insulin=str(self.requested_insulin),
carbs=str(self.carbs or ""),
carbs=str(self.carbs or "0"), # Nightscout expects non-empty carbs
bg=str(self.bg or ""),
user_override=str(self.user_override),
extended_bolus="1" if self.extended_bolus else "0",
+1 -1
View File
@@ -48,7 +48,7 @@ class NightscoutEntry:
}
if bg:
if bg_type not in (NightscoutEntry.SENSOR, NightscoutEntry.FINGER):
raise InvalidBolusTypeException
raise InvalidBolusTypeException("bg_type: %s (%s)" % (bg_type, data))
data.update({
"glucose": str(bg),
+2 -2
View File
@@ -131,10 +131,10 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
if BOLUS in features:
bolusEvents = []
if ciqBolusData:
bolusEvents = process_bolus_therapy_events(ciqBolusData)
bolusEvents = process_bolus_events(ciqBolusData, source="ciq")
if csvBolusData and not bolusEvents:
bolusEvents = process_bolus_events(csvBolusData)
bolusEvents = process_bolus_events(csvBolusData, source="csv")
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:
+12 -5
View File
@@ -1,5 +1,6 @@
import arrow
import logging
from tconnectsync.domain.bolus import Bolus
from tconnectsync.sync.cgm import find_event_at
@@ -15,17 +16,23 @@ logger = logging.getLogger(__name__)
"""
Given bolus data input from the therapy timeline CSV, converts it into a digestable format.
"""
def process_bolus_events(bolusdata, cgmEvents=None):
def process_bolus_events(bolusdata, cgmEvents=None, source=""):
bolusEvents = []
for b in bolusdata:
parsed = TConnectEntry.parse_bolus_entry(b)
parsed = None
if source == "ciq":
parsed = b.to_bolus()
else:
parsed = TConnectEntry.parse_bolus_entry(b)
assert type(parsed) == Bolus
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)
else:
logger.warning("Skipping non-completed bolus data (was a bolus in progress?): %s parsed: %s" % (b, parsed))
logger.warning("Skipping non-completed %s bolus data (was a bolus in progress?): %s parsed: %s" % (source, b, parsed))
continue
if parsed.bg and cgmEvents:
@@ -34,7 +41,7 @@ def process_bolus_events(bolusdata, cgmEvents=None):
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.is_extended_bolus else event.bolex_start_time))
return bolusEvents
@@ -72,7 +79,7 @@ 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.is_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))
+11 -4
View File
@@ -180,7 +180,7 @@ class TestBolusTherapyEvent(unittest.TestCase):
completion_time="2022-07-21 11:55:24-04:00",
insulin="2.9",
requested_insulin="2.9",
carbs="",
carbs="0",
bg="254",
user_override="0",
extended_bolus="0",
@@ -263,7 +263,7 @@ class TestBolusTherapyEvent(unittest.TestCase):
completion_time="2022-08-09 23:20:04-04:00",
insulin="0.2",
requested_insulin="0.2",
carbs="",
carbs="0",
bg="131",
user_override="1",
extended_bolus="1",
@@ -351,10 +351,17 @@ class TestBolusTherapyEvent(unittest.TestCase):
completion_time="2022-08-09 23:20:04-04:00",
insulin="0.2",
requested_insulin="0.2",
carbs="",
carbs="0",
bg="131",
user_override="1",
extended_bolus="1",
bolex_completion_time="2022-08-09 23:35:03-04:00",
bolex_start_time="2022-08-09 23:20:04-04:00"
)))
)))
BOLUS_FULL_EXAMPLES = [
TestBolusTherapyEvent.standardJson,
TestBolusTherapyEvent.correctionJson,
TestBolusTherapyEvent.extendedBolusJson
]
+56 -2
View File
@@ -8,6 +8,7 @@ import copy
from tconnectsync.process import process_time_range
from tconnectsync.parser.nightscout import EXERCISE_EVENTTYPE, IOB_ACTIVITYTYPE, SLEEP_EVENTTYPE, NightscoutEntry
from tconnectsync.features import BASAL, BOLUS, IOB, PUMP_EVENTS
from tests.domain.test_therapy_event import BOLUS_FULL_EXAMPLES, TestCGMTherapyEvent
from .api.fake import TConnectApi
from .nightscout_fake import NightscoutApi
@@ -20,6 +21,11 @@ class TestProcessTimeRange(unittest.TestCase):
def stub_therapy_timeline(self, time_start, time_end):
return copy.deepcopy(TestBasalSync.base)
def stub_ciq_therapy_events(self, time_start, time_end):
return {
"event": []
}
def stub_therapy_timeline_csv(self, time_start, time_end):
return {
@@ -53,6 +59,7 @@ class TestProcessTimeRange(unittest.TestCase):
return TestBasalSync.get_example_ciq_basal_events()
tconnect.controliq.therapy_timeline = fake_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
nightscout = NightscoutApi()
@@ -88,6 +95,7 @@ class TestProcessTimeRange(unittest.TestCase):
return TestBasalSync.get_example_ciq_basal_events()
tconnect.controliq.therapy_timeline = fake_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
nightscout = NightscoutApi()
@@ -116,6 +124,7 @@ class TestProcessTimeRange(unittest.TestCase):
return TestBasalSync.get_example_ciq_basal_events()
tconnect.controliq.therapy_timeline = fake_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
nightscout = NightscoutApi()
@@ -159,6 +168,7 @@ class TestProcessTimeRange(unittest.TestCase):
return TestBasalSync.get_example_ciq_basal_events()
tconnect.controliq.therapy_timeline = fake_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
nightscout = NightscoutApi()
@@ -193,8 +203,8 @@ class TestProcessTimeRange(unittest.TestCase):
})
self.assertListEqual(nightscout.deleted_entries, [])
"""No data in Nightscout. Uploads all bolus data from tconnect."""
def test_new_ciq_bolus_data(self):
"""No data in Nightscout. Uploads all bolus data from tconnect via the WS2 API."""
def test_new_ciq_bolus_data_from_ws2(self):
tconnect = TConnectApi()
# datetimes are unused by the API fake
@@ -202,6 +212,7 @@ class TestProcessTimeRange(unittest.TestCase):
end = datetime.datetime(2021, 4, 21, 12, 0)
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
bolusData = TestBolusSync.get_example_csv_bolus_events()
def fake_therapy_timeline_csv(time_start, time_end):
@@ -231,6 +242,46 @@ class TestProcessTimeRange(unittest.TestCase):
self.assertDictEqual(nightscout.put_entries, {})
self.assertListEqual(nightscout.deleted_entries, [])
"""No data in Nightscout. Uploads all bolus data from tconnect via CIQ therapy_events."""
def test_new_ciq_bolus_data_from_ciq_therapy_events(self):
tconnect = TConnectApi()
# datetimes are unused by the API fake
start = datetime.datetime(2022, 8, 9, 12, 0)
end = datetime.datetime(2021, 8, 10, 12, 0)
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
def fake_ciq_therapy_events(time_start, time_end):
return {
"event": BOLUS_FULL_EXAMPLES
}
tconnect.controliq.therapy_events = fake_ciq_therapy_events
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
nightscout = NightscoutApi()
nightscout.last_uploaded_entry = self.stub_last_uploaded_entry
nightscout.last_uploaded_activity = self.stub_last_uploaded_activity
process_time_range(tconnect, nightscout, start, end, pretend=False, features=[BOLUS, BASAL])
pprint.pprint(nightscout.uploaded_entries)
self.assertEqual(len(nightscout.uploaded_entries["treatments"]), len(BOLUS_FULL_EXAMPLES))
self.assertDictEqual(dict(nightscout.uploaded_entries), {
"treatments": [
# BGs are excluded because BG is not enabled in features for this test
NightscoutEntry.bolus(2.9, 0, "2022-07-21 11:55:24-04:00", notes="Automatic Bolus/Correction"),
NightscoutEntry.bolus(4.17, 25, "2022-07-21 12:29:21-04:00", notes="Standard"),
# NOTE: the extended bolus is 0.2+0.2 but we currently only surface standard
# BUG: inconsistency: we use the completion timestamp as the event timestamp for standard boluses,
# but the standard strand time for extended
NightscoutEntry.bolus(0.2, 0, "2022-08-09 23:20:04-04:00", notes="Extended 50.00%/0.00 (Override) (Extended)"),
]})
self.assertDictEqual(nightscout.put_entries, {})
self.assertListEqual(nightscout.deleted_entries, [])
"""No data in Nightscout. Nothing should be updated in Nightscout without the BOLUS feature."""
def test_bolus_data_not_updated_without_feature(self):
tconnect = TConnectApi()
@@ -271,6 +322,7 @@ class TestProcessTimeRange(unittest.TestCase):
end = datetime.datetime(2021, 4, 21, 12, 0)
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
iobData = TestIOBSync.get_example_csv_iob_events()
def fake_therapy_timeline_csv(time_start, time_end):
@@ -307,6 +359,7 @@ class TestProcessTimeRange(unittest.TestCase):
end = datetime.datetime(2021, 4, 21, 12, 0)
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
iobData = TestIOBSync.get_example_csv_iob_events()
def fake_therapy_timeline_csv(time_start, time_end):
@@ -454,6 +507,7 @@ class TestProcessTimeRange(unittest.TestCase):
}
tconnect.controliq.therapy_timeline = fake_therapy_timeline
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
tconnect.ws2.basalsuspension = self.stub_ws2_basalsuspension