mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
Fix event-processing correctness bugs found during test audit
- process_basal / process_user_mode: use timedelta.total_seconds() instead of .seconds so durations spanning >=24h (and negative deltas) are correct. - process_cartridge: report cartridge fill from insulinVolume (v2Volume is 0 on real pumps); treat tubing primeSize -1 as 'not recorded'; format cannula primeSize with %.1f instead of %d. - process_bolus: no longer drop the extended portion of a combo bolus. The initial portion is emitted as before; the extended portion (LidBolexCompleted) is added as a separate treatment at its completion time. - check.py: return after a config ImportError instead of falling through to an unbound-name NameError.
This commit is contained in:
@@ -61,6 +61,9 @@ def check_login(tconnect, time_start, time_end, verbose=False, sanitize=True):
|
||||
except ImportError as e:
|
||||
log("Error: Unable to load config file. Please check your .env file or environment variables")
|
||||
log_err(e)
|
||||
# Config never loaded; the names below are unbound, so stop here instead
|
||||
# of crashing with a NameError.
|
||||
return
|
||||
|
||||
log(f"Using {TCONNECT_REGION=}")
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ class ProcessBasal:
|
||||
return None
|
||||
return NightscoutEntry.basal(
|
||||
value = value,
|
||||
duration_mins = duration.seconds / 60,
|
||||
duration_mins = duration.total_seconds() / 60,
|
||||
created_at = start.format(),
|
||||
reason = ', '.join(bitmask_to_list(event.changetype)),
|
||||
pump_event_id = "%s" % event.seqNum
|
||||
@@ -101,7 +101,7 @@ class ProcessBasal:
|
||||
return None
|
||||
return NightscoutEntry.basal(
|
||||
value = value,
|
||||
duration_mins = duration.seconds / 60,
|
||||
duration_mins = duration.total_seconds() / 60,
|
||||
created_at = start.format(),
|
||||
reason = ', '.join(bitmask_to_list(event.commandedRateSource)),
|
||||
pump_event_id = "%s" % event.seqNum
|
||||
|
||||
@@ -40,33 +40,36 @@ class ProcessBolus:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
logger.info("Last Nightscout bolus upload: %s" % last_upload_time)
|
||||
|
||||
# TODO EXTENDED BOLUSES
|
||||
bolusCompletedEvents = []
|
||||
# Correlate a bolus's request/completion messages by bolusid.
|
||||
bolusEventsForId = {}
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if event.bolusid not in bolusEventsForId.keys():
|
||||
bolusEventsForId[event.bolusid] = {}
|
||||
|
||||
bolusEventsForId[event.bolusid][type(event)] = event
|
||||
|
||||
if type(event) == eventtypes.LidBolusCompleted:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping bolusCompletedEvent not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
bolusCompletedEvents.append(event)
|
||||
|
||||
bolusCompletedEvents.sort(key=lambda e: e.eventTimestamp)
|
||||
bolusEventsForId.setdefault(event.bolusid, {})[type(event)] = event
|
||||
|
||||
# Emit one Nightscout treatment per completion event, each at its own time:
|
||||
# - LidBolusCompleted -> the standard / "now" bolus (carbs, bg, notes)
|
||||
# - LidBolexCompleted -> the extended portion of a combo bolus (added
|
||||
# separately, insulin only, so its later delivery is not dropped).
|
||||
completions = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if type(event) not in (eventtypes.LidBolusCompleted, eventtypes.LidBolexCompleted):
|
||||
continue
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping bolus completion not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
completions.append(event)
|
||||
|
||||
completions.sort(key=lambda e: e.eventTimestamp)
|
||||
|
||||
ns_entries = []
|
||||
for bolusCompleted in bolusCompletedEvents:
|
||||
m = bolusEventsForId[bolusCompleted.bolusid]
|
||||
for event in completions:
|
||||
if type(event) == eventtypes.LidBolexCompleted:
|
||||
ns_entries.append(self.bolex_to_nsentry(event))
|
||||
continue
|
||||
|
||||
m = bolusEventsForId[event.bolusid]
|
||||
ns_entries.append(self.bolus_to_nsentry(
|
||||
bolusCompleted,
|
||||
event,
|
||||
bolusRequested1 = m.get(eventtypes.LidBolusRequestedMsg1),
|
||||
bolusRequested2 = m.get(eventtypes.LidBolusRequestedMsg2),
|
||||
bolusRequested3 = m.get(eventtypes.LidBolusRequestedMsg3),
|
||||
@@ -116,3 +119,16 @@ class ProcessBolus:
|
||||
pump_event_id = ",".join(seq_nums)
|
||||
)
|
||||
|
||||
def bolex_to_nsentry(self, bolexCompleted: "BaseEvent") -> Optional[dict]:
|
||||
# The extended portion of a combo bolus, added as its own treatment at
|
||||
# the time it finished delivering. Insulin only; carbs/bg belong to the
|
||||
# initial LidBolusCompleted entry and must not be double-counted here.
|
||||
return NightscoutEntry.bolus(
|
||||
bolus = insulin_float_round(bolexCompleted.insulindelivered),
|
||||
carbs = None,
|
||||
created_at = bolexCompleted.eventTimestamp.format(),
|
||||
notes = "Extended Bolus",
|
||||
bg = None,
|
||||
pump_event_id = "%s" % bolexCompleted.seqNum
|
||||
)
|
||||
|
||||
|
||||
@@ -85,22 +85,28 @@ class ProcessCartridge:
|
||||
return count
|
||||
|
||||
def cart_to_nsentry(self, cartFilled: "BaseEvent") -> Optional[dict]:
|
||||
# insulinVolume is populated on t:slim X2 / Mobi; v2Volume is a legacy fallback.
|
||||
volume = cartFilled.insulinvolume or cartFilled.v2Volume
|
||||
return NightscoutEntry.sitechange(
|
||||
created_at = cartFilled.eventTimestamp.format(),
|
||||
reason = "Cartridge Filled" + (" (%du filled)" % round(cartFilled.v2Volume) if cartFilled.v2Volume else ""),
|
||||
reason = "Cartridge Filled" + (" (%du filled)" % round(volume) if volume else ""),
|
||||
pump_event_id = "%s" % cartFilled.seqNum
|
||||
)
|
||||
|
||||
def cannula_to_nsentry(self, cannulaFilled: "BaseEvent") -> Optional[dict]:
|
||||
# primeSize is fractional (e.g. 0.3u); format with one decimal, not %d.
|
||||
primed = cannulaFilled.primesize if cannulaFilled.primesize and cannulaFilled.primesize > 0 else None
|
||||
return NightscoutEntry.sitechange(
|
||||
created_at = cannulaFilled.eventTimestamp.format(),
|
||||
reason = "Cannula Filled" + (" (%du primed)" % round(cannulaFilled.primesize, 2) if cannulaFilled.primesize else ""),
|
||||
reason = "Cannula Filled" + (" (%.1fu primed)" % primed if primed else ""),
|
||||
pump_event_id = "%s" % cannulaFilled.seqNum
|
||||
)
|
||||
|
||||
def tubing_to_nsentry(self, tubingFilled: "BaseEvent") -> Optional[dict]:
|
||||
# primeSize is -1 (sentinel, "not recorded") on real tubing fills; only show a real prime volume.
|
||||
primed = tubingFilled.primesize if tubingFilled.primesize and tubingFilled.primesize > 0 else None
|
||||
return NightscoutEntry.sitechange(
|
||||
created_at = tubingFilled.eventTimestamp.format(),
|
||||
reason = "Tubing Filled" + (" (%du primed)" % round(tubingFilled.primesize) if tubingFilled.primesize else ""),
|
||||
reason = "Tubing Filled" + (" (%du primed)" % round(primed) if primed else ""),
|
||||
pump_event_id = "%s" % tubingFilled.seqNum
|
||||
)
|
||||
|
||||
@@ -157,7 +157,7 @@ class ProcessUserMode:
|
||||
elif start.activesleepschedule:
|
||||
reason = "Sleep (Scheduled)"
|
||||
|
||||
duration_mins = (stop.eventTimestamp - start.eventTimestamp).seconds / 60
|
||||
duration_mins = (stop.eventTimestamp - start.eventTimestamp).total_seconds() / 60
|
||||
return NightscoutEntry.activity(
|
||||
created_at=start.eventTimestamp.format(),
|
||||
reason=reason,
|
||||
@@ -172,7 +172,7 @@ class ProcessUserMode:
|
||||
elif start.activesleepscheduleRaw:
|
||||
reason = "Sleep (Scheduled)"
|
||||
|
||||
duration_mins = (time_end - start.eventTimestamp).seconds / 60
|
||||
duration_mins = (time_end - start.eventTimestamp).total_seconds() / 60
|
||||
return NightscoutEntry.activity(
|
||||
created_at=start.eventTimestamp.format(),
|
||||
reason=reason + " - " + NOT_ENDED if reason else NOT_ENDED,
|
||||
@@ -191,7 +191,7 @@ class ProcessUserMode:
|
||||
if stop.exercisestoppedbytimer == eventtypes.LidAaUserModeChange.ExercisestoppedbytimerEnum.TrueVal:
|
||||
reason += " (Stopped by timer)"
|
||||
|
||||
duration_mins = (stop.eventTimestamp - start.eventTimestamp).seconds / 60
|
||||
duration_mins = (stop.eventTimestamp - start.eventTimestamp).total_seconds() / 60
|
||||
return NightscoutEntry.activity(
|
||||
created_at=start.eventTimestamp.format(),
|
||||
reason=reason,
|
||||
@@ -204,7 +204,7 @@ class ProcessUserMode:
|
||||
if start.exercisechoice == eventtypes.LidAaUserModeChange.ExercisechoiceEnum.Timed:
|
||||
reason = "Exercise (Timed)"
|
||||
|
||||
duration_mins = (time_end - start.eventTimestamp).seconds / 60
|
||||
duration_mins = (time_end - start.eventTimestamp).total_seconds() / 60
|
||||
return NightscoutEntry.activity(
|
||||
created_at=start.eventTimestamp.format(),
|
||||
reason=reason + " - " + NOT_ENDED,
|
||||
@@ -220,7 +220,7 @@ class ProcessUserMode:
|
||||
else:
|
||||
self.nightscout.delete_entry('treatments/%s' % sleep_last_upload["_id"])
|
||||
|
||||
duration_mins = (event.eventTimestamp - arrow.get(sleep_last_upload["created_at"])).seconds / 60
|
||||
duration_mins = (event.eventTimestamp - arrow.get(sleep_last_upload["created_at"])).total_seconds() / 60
|
||||
return NightscoutEntry.activity(
|
||||
created_at=sleep_last_upload["created_at"],
|
||||
reason=sleep_last_upload["reason"].replace(" - %s" % NOT_ENDED, ""),
|
||||
@@ -240,7 +240,7 @@ class ProcessUserMode:
|
||||
if event.exercisestoppedbytimer == eventtypes.LidAaUserModeChange.ExercisestoppedbytimerEnum.TrueVal:
|
||||
reason += " (Stopped by timer)"
|
||||
|
||||
duration_mins = (event.eventTimestamp - arrow.get(exercise_last_upload["created_at"])).seconds / 60
|
||||
duration_mins = (event.eventTimestamp - arrow.get(exercise_last_upload["created_at"])).total_seconds() / 60
|
||||
return NightscoutEntry.activity(
|
||||
created_at=exercise_last_upload["created_at"],
|
||||
reason=reason,
|
||||
|
||||
Reference in New Issue
Block a user