Report CGM out-of-range readings as LOW/HIGH sentinels

The bff pump-logs endpoint gives glucoseValueStatus + a raw display value; a
below/above-range reading (e.g. status SpecialLow with displayValue 38) is a
boundary indicator, not a measurement. Mirror the Tandem Source frontend
(CgmBuilder.determineGlucoseValue): map SpecialLow/precise<40 -> 39 and
SpecialHigh/precise>400 -> 401. Each sensor (G7/G6/FSL2/FSL3) is resolved
against its own glucoseValueStatus enum members rather than assuming the enums
are consistent across sensor types.

Also widen the real-JSON CGM tests to span glucose 38..361 (incl. the LOW
sentinel) and add a ProcessTimeRange basal JSON integration test.
This commit is contained in:
James Woglom
2026-07-01 03:03:26 +00:00
parent 69637015e2
commit a977bd65f9
3 changed files with 165 additions and 18 deletions
@@ -5,17 +5,69 @@ from ...features import DEFAULT_FEATURES
from ... import features
from ... import secret
from ...eventparser.raw_event import TANDEM_EPOCH
from ...eventparser import events as eventtypes
from ...parser.nightscout import NightscoutEntry
from typing import Iterable, List, Optional, TYPE_CHECKING
from typing import Iterable, List, Optional, Union, TYPE_CHECKING
if TYPE_CHECKING:
from ...api import TConnectApi
from ...nightscout import NightscoutApi
from ...eventparser.raw_event import BaseEvent
# The four CGM-reading event types share the glucoseValueStatus /
# currentGlucoseDisplayValue fields determine_glucose_value() reads.
CgmReadingEvent = Union[
eventtypes.LidCgmDataG7,
eventtypes.LidCgmDataGxb,
eventtypes.LidCgmDataFsl2,
eventtypes.LidCgmDataFsl3,
]
logger = logging.getLogger(__name__)
# Mirrors the Tandem Source frontend (CgmBuilder.determineGlucoseValue): out-of-range
# and special readings are reported as sentinel values rather than the raw display value.
GLUCOSE_LIMIT_LOW = 40
GLUCOSE_LIMIT_HIGH = 400
GLUCOSE_VALUE_LOW = 39
GLUCOSE_VALUE_HIGH = 401
def _resolve_glucose_value(display_value, status, precise, high, low):
if status == high:
return GLUCOSE_VALUE_HIGH
if status == low:
return GLUCOSE_VALUE_LOW
if status == precise:
if display_value < GLUCOSE_LIMIT_LOW:
return GLUCOSE_VALUE_LOW
if display_value > GLUCOSE_LIMIT_HIGH:
return GLUCOSE_VALUE_HIGH
return display_value
# Each sensor is handled separately: the glucoseValueStatus enums are NOT assumed
# to be consistent across sensor types (e.g. G6 names its members differently), so
# every branch resolves against that sensor's own enum members.
def determine_glucose_value(event: CgmReadingEvent) -> int:
display_value = event.currentglucosedisplayvalue
status = event.glucosevaluestatus
if isinstance(event, eventtypes.LidCgmDataG7):
e = eventtypes.LidCgmDataG7.GlucosevaluestatusEnum
return _resolve_glucose_value(display_value, status, e.PreciseValue, e.SpecialHigh, e.SpecialLow)
if isinstance(event, eventtypes.LidCgmDataGxb):
e = eventtypes.LidCgmDataGxb.GlucosevaluestatusEnum
return _resolve_glucose_value(display_value, status,
e.CurrentglucosedisplayvalueContainsTheGlucoseReading,
e.TheGlucoseReadingIsHigh, e.TheGlucoseReadingIsLow)
if isinstance(event, eventtypes.LidCgmDataFsl3):
e = eventtypes.LidCgmDataFsl3.GlucosevaluestatusEnum
return _resolve_glucose_value(display_value, status, e.PreciseValue, e.SpecialHigh, e.SpecialLow)
if isinstance(event, eventtypes.LidCgmDataFsl2):
e = eventtypes.LidCgmDataFsl2.GlucosevaluestatusEnum
return _resolve_glucose_value(display_value, status, e.PreciseValue, e.SpecialHigh, e.SpecialLow)
return display_value
class ProcessCGMReading:
def __init__(self, tconnect: "TConnectApi", nightscout: "NightscoutApi", tconnect_device_id: str, pretend: bool, features: List[str] = DEFAULT_FEATURES, timezone: Optional[str] = None) -> None:
self.tconnect = tconnect
@@ -72,7 +124,7 @@ class ProcessCGMReading:
def to_nsentry(self, event: "BaseEvent") -> Optional[dict]:
return NightscoutEntry.entry(
sgv = event.currentglucosedisplayvalue,
sgv = determine_glucose_value(event),
created_at = self.timestamp_for(event).format(),
pump_event_id = "%s" % event.seqNum,
)
+73 -1
View File
@@ -5,7 +5,7 @@ import arrow
from tconnectsync.sync.tandemsource.process import ProcessTimeRange
from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.generic import Event
from tconnectsync.eventparser.generic import Event, Events
from ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi
@@ -146,5 +146,77 @@ class TestProcessTimeRangeBasalDuration(unittest.TestCase):
self.assertEqual(basal_2['duration'], 5.0)
BASAL_JSON_1 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 279, "sequenceGroup": 0, "sequenceNumber": 417004, "pumpDateTime": "2026-05-07T00:01:11", "eventProperties": {"commandedRateSource": 3, "reservedA2": 3, "spareA3": 0, "commandedRate": 1000, "profileBasalRate": 1000, "algorithmRate": 1000, "tempRate": 65535}, "estimatedDateTime": "2026-05-07T00:01:11Z"}
BASAL_JSON_2 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 279, "sequenceGroup": 0, "sequenceNumber": 417029, "pumpDateTime": "2026-05-07T00:06:12", "eventProperties": {"commandedRateSource": 3, "reservedA2": 0, "spareA3": 0, "commandedRate": 1000, "profileBasalRate": 1000, "algorithmRate": 1000, "tempRate": 65535}, "estimatedDateTime": "2026-05-07T00:06:12Z"}
class TestProcessTimeRangeJsonBasal(unittest.TestCase):
"""Basal delivery events processed via real pump-logs JSON (production path)."""
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.tconnect._tandemsource = FakeTandemSourceApi()
self.nightscout = NightscoutApi()
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
self.secret = build_secrets(
FETCH_ALL_EVENT_TYPES=False
)
self.tconnectDevice = {
'deviceId': 'test',
'maxDateWithEvents': '2026-05-07T00:20:00-04:00'
}
self.process = ProcessTimeRange(
self.tconnect,
self.nightscout,
self.tconnectDevice,
pretend=False,
secret=self.secret
)
self.tconnect._tandemsource.events = list(Events([dict(BASAL_JSON_1), dict(BASAL_JSON_2)]))
def test_basal_events_via_json(self):
time_start = arrow.get('2026-05-07T00:00:00-04:00')
time_end = arrow.get('2026-05-07T00:11:12-04:00')
count, last_seqnum = self.process.process(time_start, time_end)
self.assertEqual(count, 2)
self.assertEqual(last_seqnum, 417029)
treatments = self.nightscout.uploaded_entries['treatments']
self.assertEqual(len(treatments), 2)
self.assertDictEqual(treatments[0], {
"eventType": "Temp Basal",
"reason": "Algorithm",
"duration": 5.016666666666667,
"absolute": 1.0,
"rate": 1.0,
"created_at": "2026-05-07 00:01:11-04:00",
"carbs": None,
"insulin": None,
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": "417004"
})
self.assertDictEqual(treatments[1], {
"eventType": "Temp Basal",
"reason": "Algorithm",
"duration": None,
"absolute": 1.0,
"rate": 1.0,
"created_at": "2026-05-07 00:06:12-04:00",
"carbs": None,
"insulin": None,
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": "417029"
})
if __name__ == '__main__':
unittest.main()
@@ -422,9 +422,12 @@ class TestProcessCGMReadingFSL3(unittest.TestCase):
# Real LID_CGM_DATA_G7 pump-logs JSON events captured from a live Mobi account
# (deviceAssignmentId redacted). These exercise the production path
# (Events -> ProcessCGMReading) rather than the binary decoder.
G7_JSON_1 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 484329, "pumpDateTime": "2026-05-26T00:04:12", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": -3, "algorithmState": 32, "rssi": -82, "currentGlucoseDisplayValue": 347, "egvTimeStamp": 580608249, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-26T00:04:12Z"}
G7_JSON_2 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 484338, "pumpDateTime": "2026-05-26T00:09:12", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": -4, "algorithmState": 32, "rssi": -84, "currentGlucoseDisplayValue": 345, "egvTimeStamp": 580608548, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-26T00:09:12Z"}
G7_JSON_3 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 484351, "pumpDateTime": "2026-05-26T00:14:12", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": -4, "algorithmState": 32, "rssi": -78, "currentGlucoseDisplayValue": 341, "egvTimeStamp": 580608849, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-26T00:14:12Z"}
G7_JSON_1 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 416999, "pumpDateTime": "2026-05-07T00:01:04", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": 8, "algorithmState": 32, "rssi": -59, "currentGlucoseDisplayValue": 249, "egvTimeStamp": 578966461, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-07T00:01:04Z"}
G7_JSON_2 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 417212, "pumpDateTime": "2026-05-07T01:06:04", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": -12, "algorithmState": 32, "rssi": -57, "currentGlucoseDisplayValue": 193, "egvTimeStamp": 578970361, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-07T01:06:04Z"}
G7_JSON_3 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 417300, "pumpDateTime": "2026-05-07T01:51:04", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": -10, "algorithmState": 32, "rssi": -51, "currentGlucoseDisplayValue": 118, "egvTimeStamp": 578973061, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-07T01:51:04Z"}
G7_JSON_4 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 440616, "pumpDateTime": "2026-05-13T19:46:30", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": 5, "algorithmState": 32, "rssi": -67, "currentGlucoseDisplayValue": 321, "egvTimeStamp": 579555987, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-13T19:46:30Z"}
G7_JSON_5 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 450283, "pumpDateTime": "2026-05-16T15:34:52", "eventProperties": {"glucoseValueStatus": 2, "cgmDataType": [0], "rate": -5, "algorithmState": 32, "rssi": -52, "currentGlucoseDisplayValue": 38, "egvTimeStamp": 579800089, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-16T15:34:52Z"}
G7_JSON_6 = {"deviceAssignmentId": "00000000-0000-0000-0000-000000000000", "eventCode": 399, "sequenceGroup": 0, "sequenceNumber": 483972, "pumpDateTime": "2026-05-25T22:14:11", "eventProperties": {"glucoseValueStatus": 0, "cgmDataType": [0], "rate": 15, "algorithmState": 32, "rssi": -79, "currentGlucoseDisplayValue": 361, "egvTimeStamp": 580601648, "egvInfoBitmask": [0, 5, 6, 7, 8, 11, 12], "interval": 0, "reservedD15": 0}, "estimatedDateTime": "2026-05-25T22:14:11Z"}
class TestProcessCGMReadingG7Json(unittest.TestCase):
@@ -440,28 +443,48 @@ class TestProcessCGMReadingG7Json(unittest.TestCase):
def test_single_g7_json_reading(self):
events = list(Events([dict(G7_JSON_1)]))
self.assertEqual(type(events[0]), eventtypes.LidCgmDataG7)
self.assertEqual(events[0].currentglucosedisplayvalue, 347)
self.assertEqual(events[0].egvTimestamp, 580608249)
self.assertEqual(events[0].egvTimestamp, 578966461)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 1)
self.assertEqual(p[0]['sgv'], 347)
# created_at is derived from egvTimeStamp (3s before pumpDateTime here)
self.assertEqual(p[0]['dateString'], '2026-05-26T00:04:09-0400')
self.assertEqual(p[0]['pump_event_id'], '484329')
self.assertEqual(p[0]['sgv'], 249)
self.assertEqual(p[0]['dateString'], '2026-05-07T00:01:01-0400')
self.assertEqual(p[0]['pump_event_id'], '416999')
def test_multiple_g7_json_readings(self):
events = list(Events([dict(G7_JSON_1), dict(G7_JSON_2), dict(G7_JSON_3)]))
def test_diverse_glucose_range(self):
events = list(Events([
dict(G7_JSON_1), dict(G7_JSON_2), dict(G7_JSON_3),
dict(G7_JSON_4), dict(G7_JSON_5), dict(G7_JSON_6)
]))
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual([e['sgv'] for e in p], [347, 345, 341])
self.assertEqual([e['pump_event_id'] for e in p], ['484329', '484338', '484351'])
# The SpecialLow reading (raw 38) is reported as the LOW sentinel 39.
self.assertEqual([e['sgv'] for e in p], [249, 193, 118, 321, 39, 361])
self.assertEqual([e['dateString'] for e in p], [
'2026-05-07T00:01:01-0400',
'2026-05-07T01:06:01-0400',
'2026-05-07T01:51:01-0400',
'2026-05-13T19:46:27-0400',
'2026-05-16T15:34:49-0400',
'2026-05-25T22:14:08-0400',
])
self.assertEqual([e['pump_event_id'] for e in p], [
'416999', '417212', '417300', '440616', '450283', '483972'
])
def test_special_low_reading(self):
# glucoseValueStatus SpecialLow reports the LOW sentinel (39), not the
# raw below-range display value (38).
events = list(Events([dict(G7_JSON_5)]))
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 1)
self.assertEqual(p[0]['sgv'], 39)
def test_skips_readings_at_or_before_last_upload(self):
# Only readings strictly after the last Nightscout upload are returned.
self.nightscout.last_uploaded_bg_entry = lambda *args, **kwargs: {'dateString': '2026-05-26T00:09:08-0400'}
self.nightscout.last_uploaded_bg_entry = lambda *args, **kwargs: {'dateString': '2026-05-07T01:06:01-0400'}
events = list(Events([dict(G7_JSON_1), dict(G7_JSON_2), dict(G7_JSON_3)]))
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual([e['sgv'] for e in p], [341])
self.assertEqual([e['sgv'] for e in p], [118])
if __name__ == '__main__':