Fold pump-logs JSON handling into Event()/Events()

Event(x) and Events(x) now accept either a raw binary event/stream or a
pump-logs JSON event dict / iterable of dicts, dispatching on input type,
replacing the separate Event_from_json/Events_from_json functions. Point
pump_events() and all tests at the unified entry points, and add real-JSON
sync tests for CGM readings, user-mode sleep/exercise, and alarms alongside
the existing binary-fixture tests.
This commit is contained in:
James Woglom
2026-07-01 02:52:16 +00:00
parent d1d67b5042
commit 69637015e2
7 changed files with 204 additions and 50 deletions
+2 -2
View File
@@ -25,7 +25,7 @@ from jwt.algorithms import RSAAlgorithm
from ..util import timeago, cap_length from ..util import timeago, cap_length
from .common import parse_ymd_date, base_headers, base_session, ApiException, ApiLoginException from .common import parse_ymd_date, base_headers, base_session, ApiException, ApiLoginException
from ..secret import CACHE_CREDENTIALS, CACHE_CREDENTIALS_PATH from ..secret import CACHE_CREDENTIALS, CACHE_CREDENTIALS_PATH
from ..eventparser.generic import Events_from_json from ..eventparser.generic import Events
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -676,6 +676,6 @@ class TandemSourceApi:
# clockChanges (LID_TIME_CHANGED/LID_DATE_CHANGED) are not consumed by any # clockChanges (LID_TIME_CHANGED/LID_DATE_CHANGED) are not consumed by any
# processor, so they are counted for visibility but not parsed. # processor, so they are counted for visibility but not parsed.
logger.info(f"Read {len(events)} events ({clock_change_count} clock changes skipped)") logger.info(f"Read {len(events)} events ({clock_change_count} clock changes skipped)")
return Events_from_json(events) return Events(events)
+16 -18
View File
@@ -11,33 +11,31 @@ from .utils import batched
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def Event(x): def Event(x):
# Accepts either a 26-byte binary event or a pump-logs JSON event (dict).
if isinstance(x, dict):
raw_event = RawEvent.build_from_json(x)
if not raw_event.id in EVENT_IDS:
# Log unknown events with their property keys for reverse-engineering
props = ' '.join(x['eventProperties'].keys())
logger.debug(f"UNKNOWN_JSON_EVENT | id={raw_event.id} | seqNum={raw_event.seqNum} | timestamp={raw_event.timestamp.isoformat()} | props={props}")
return raw_event
return EVENT_IDS[raw_event.id].build_from_json(x)
raw_event = RawEvent.build(x) raw_event = RawEvent.build(x)
if not raw_event.id in EVENT_IDS: if not raw_event.id in EVENT_IDS:
# Log unknown events with full hex dump for reverse-engineering # Log unknown events with full hex dump for reverse-engineering
hex_dump = ' '.join(f'{b:02x}' for b in x[:EVENT_LEN]) hex_dump = ' '.join(f'{b:02x}' for b in x[:EVENT_LEN])
# Also log seqNum and timestamp for correlation
logger.debug(f"UNKNOWN_EVENT | id={raw_event.id} | seqNum={raw_event.seqNum} | timestamp={raw_event.timestamp.isoformat()} | bytes={hex_dump}") logger.debug(f"UNKNOWN_EVENT | id={raw_event.id} | seqNum={raw_event.seqNum} | timestamp={raw_event.timestamp.isoformat()} | bytes={hex_dump}")
return raw_event return raw_event
return EVENT_IDS[raw_event.id].build(x) return EVENT_IDS[raw_event.id].build(x)
Events = lambda x: (Event(bytearray(e)) for e in batched(x, EVENT_LEN)) def Events(x):
# Accepts either a raw binary event stream or an iterable of pump-logs JSON events.
if isinstance(x, (bytes, bytearray)):
return (Event(bytearray(e)) for e in batched(x, EVENT_LEN))
return (Event(e) for e in x)
def decode_raw_events(raw): def decode_raw_events(raw):
return base64.b64decode(raw) return base64.b64decode(raw)
def Event_from_json(event):
raw_event = RawEvent.build_from_json(event)
if not raw_event.id in EVENT_IDS:
# Log unknown events with their property keys for reverse-engineering
props = ' '.join(event['eventProperties'].keys())
# Also log seqNum and timestamp for correlation
logger.debug(f"UNKNOWN_JSON_EVENT | id={raw_event.id} | seqNum={raw_event.seqNum} | timestamp={raw_event.timestamp.isoformat()} | props={props}")
return raw_event
return EVENT_IDS[raw_event.id].build_from_json(event)
Events_from_json = lambda events: (Event_from_json(e) for e in events)
+21 -21
View File
@@ -2,7 +2,7 @@
import unittest import unittest
from tconnectsync.eventparser.generic import Event_from_json, Events_from_json from tconnectsync.eventparser.generic import Event, Events
from tconnectsync.eventparser import events as eventtypes from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.raw_event import RawEvent from tconnectsync.eventparser.raw_event import RawEvent
@@ -70,46 +70,46 @@ class TestBuildFromJson(unittest.TestCase):
maxDiff = None maxDiff = None
def test_dispatches_to_correct_class(self): def test_dispatches_to_correct_class(self):
self.assertIsInstance(Event_from_json(BASAL_279), eventtypes.LidBasalDelivery) self.assertIsInstance(Event(BASAL_279), eventtypes.LidBasalDelivery)
self.assertIsInstance(Event_from_json(ALARM_5), eventtypes.LidAlarmActivated) self.assertIsInstance(Event(ALARM_5), eventtypes.LidAlarmActivated)
def test_plain_fields(self): def test_plain_fields(self):
ev = Event_from_json(BASAL_279) ev = Event(BASAL_279)
self.assertEqual(ev.commandedRate, 0) self.assertEqual(ev.commandedRate, 0)
self.assertEqual(ev.profileBasalRate, 1000) self.assertEqual(ev.profileBasalRate, 1000)
self.assertEqual(ev.tempRate, 65535) self.assertEqual(ev.tempRate, 65535)
def test_envelope_fields(self): def test_envelope_fields(self):
ev = Event_from_json(BASAL_279) ev = Event(BASAL_279)
self.assertEqual(ev.seqNum, 393131) self.assertEqual(ev.seqNum, 393131)
self.assertEqual(ev.eventId, 279) self.assertEqual(ev.eventId, 279)
def test_timestamp_preserves_wall_clock(self): def test_timestamp_preserves_wall_clock(self):
# eventTimestamp keeps pumpDateTime's wall-clock (tz forced to the # eventTimestamp keeps pumpDateTime's wall-clock (tz forced to the
# configured TIMEZONE_NAME), so the naive portion round-trips exactly. # configured TIMEZONE_NAME), so the naive portion round-trips exactly.
ev = Event_from_json(BASAL_279) ev = Event(BASAL_279)
self.assertEqual(ev.eventTimestamp.format('YYYY-MM-DDTHH:mm:ss'), "2026-04-30T00:03:29") self.assertEqual(ev.eventTimestamp.format('YYYY-MM-DDTHH:mm:ss'), "2026-04-30T00:03:29")
def test_missing_plain_field_defaults_to_none(self): def test_missing_plain_field_defaults_to_none(self):
event = dict(BASAL_279) event = dict(BASAL_279)
event["eventProperties"] = {k: v for k, v in BASAL_279["eventProperties"].items() if k != "tempRate"} event["eventProperties"] = {k: v for k, v in BASAL_279["eventProperties"].items() if k != "tempRate"}
ev = Event_from_json(event) ev = Event(event)
self.assertIsNone(ev.tempRate) self.assertIsNone(ev.tempRate)
self.assertEqual(ev.commandedRate, 0) # others still parse self.assertEqual(ev.commandedRate, 0) # others still parse
def test_extra_keys_are_ignored(self): def test_extra_keys_are_ignored(self):
event = dict(BASAL_279) event = dict(BASAL_279)
event["eventProperties"] = dict(BASAL_279["eventProperties"], someFutureField=42) event["eventProperties"] = dict(BASAL_279["eventProperties"], someFutureField=42)
ev = Event_from_json(event) # must not raise ev = Event(event) # must not raise
self.assertFalse(hasattr(ev, "someFutureField")) self.assertFalse(hasattr(ev, "someFutureField"))
def test_events_from_json_yields_in_order(self): def test_events_from_json_yields_in_order(self):
out = list(Events_from_json([BASAL_279, ALARM_5])) out = list(Events([BASAL_279, ALARM_5]))
self.assertEqual([type(e).__name__ for e in out], self.assertEqual([type(e).__name__ for e in out],
["LidBasalDelivery", "LidAlarmActivated"]) ["LidBasalDelivery", "LidAlarmActivated"])
def test_unknown_eventcode_falls_back_to_rawevent(self): def test_unknown_eventcode_falls_back_to_rawevent(self):
ev = Event_from_json({ ev = Event({
"eventCode": 99999, "eventCode": 99999,
"sequenceNumber": 7, "sequenceNumber": 7,
"pumpDateTime": "2026-04-30T00:00:00", "pumpDateTime": "2026-04-30T00:00:00",
@@ -128,21 +128,21 @@ class TestEnumAndRatioFields(unittest.TestCase):
def test_enum_resolves_from_raw_int(self): def test_enum_resolves_from_raw_int(self):
# commandedRateSource:3 -> Algorithm # commandedRateSource:3 -> Algorithm
ev = Event_from_json(BASAL_279) ev = Event(BASAL_279)
self.assertEqual(ev.commandedRateSourceRaw, 3) self.assertEqual(ev.commandedRateSourceRaw, 3)
self.assertEqual(ev.commandedRateSource, self.assertEqual(ev.commandedRateSource,
eventtypes.LidBasalDelivery.CommandedratesourceEnum.Algorithm) eventtypes.LidBasalDelivery.CommandedratesourceEnum.Algorithm)
def test_dictionary_enum_resolves_from_raw_int(self): def test_dictionary_enum_resolves_from_raw_int(self):
# alarmId:18 -> ResumePumpAlarm (stored on the alarmidRaw attr) # alarmId:18 -> ResumePumpAlarm (stored on the alarmidRaw attr)
ev = Event_from_json(ALARM_5) ev = Event(ALARM_5)
self.assertEqual(ev.alarmidRaw, 18) self.assertEqual(ev.alarmidRaw, 18)
self.assertEqual(ev.alarmid, self.assertEqual(ev.alarmid,
eventtypes.LidAlarmActivated.AlarmidEnum.ResumePumpAlarm) eventtypes.LidAlarmActivated.AlarmidEnum.ResumePumpAlarm)
def test_multiple_enums_on_one_event(self): def test_multiple_enums_on_one_event(self):
# requestedAction:2 -> StopSleep; previousUserMode:1 -> Sleeping # requestedAction:2 -> StopSleep; previousUserMode:1 -> Sleeping
ev = Event_from_json(UMC_229) ev = Event(UMC_229)
self.assertEqual(ev.requestedaction, self.assertEqual(ev.requestedaction,
eventtypes.LidAaUserModeChange.RequestedactionEnum.StopSleep) eventtypes.LidAaUserModeChange.RequestedactionEnum.StopSleep)
self.assertEqual(ev.previoususermode, self.assertEqual(ev.previoususermode,
@@ -150,13 +150,13 @@ class TestEnumAndRatioFields(unittest.TestCase):
def test_ratio_field_scales(self): def test_ratio_field_scales(self):
# rate:-6 -> -0.6 mg/dL/min (rateRaw ×0.1) # rate:-6 -> -0.6 mg/dL/min (rateRaw ×0.1)
ev = Event_from_json(CGM_399) ev = Event(CGM_399)
self.assertEqual(ev.rateRaw, -6) self.assertEqual(ev.rateRaw, -6)
self.assertAlmostEqual(ev.rate, -0.6) self.assertAlmostEqual(ev.rate, -0.6)
def test_enum_zero_value_resolves(self): def test_enum_zero_value_resolves(self):
# glucoseValueStatus:0 -> PreciseValue (0 must not be treated as missing) # glucoseValueStatus:0 -> PreciseValue (0 must not be treated as missing)
ev = Event_from_json(CGM_399) ev = Event(CGM_399)
self.assertEqual(ev.glucosevaluestatusRaw, 0) self.assertEqual(ev.glucosevaluestatusRaw, 0)
self.assertEqual(ev.glucosevaluestatus, self.assertEqual(ev.glucosevaluestatus,
eventtypes.LidCgmDataG7.GlucosevaluestatusEnum.PreciseValue) eventtypes.LidCgmDataG7.GlucosevaluestatusEnum.PreciseValue)
@@ -169,21 +169,21 @@ class TestBitmaskFields(unittest.TestCase):
def test_single_bit_array(self): def test_single_bit_array(self):
# activeSleepSchedule:[0] -> 1<<0 == 1 # activeSleepSchedule:[0] -> 1<<0 == 1
ev = Event_from_json(UMC_229) ev = Event(UMC_229)
self.assertEqual(ev.activesleepscheduleRaw, 1) self.assertEqual(ev.activesleepscheduleRaw, 1)
self.assertEqual(ev.activesleepschedule, self.assertEqual(ev.activesleepschedule,
eventtypes.LidAaUserModeChange.ActivesleepscheduleBitmask.SleepSchedule1IsActive) eventtypes.LidAaUserModeChange.ActivesleepscheduleBitmask.SleepSchedule1IsActive)
def test_cgm_datatype_array(self): def test_cgm_datatype_array(self):
# cgmDataType:[0] -> 1<<0 == 1 -> Fmr # cgmDataType:[0] -> 1<<0 == 1 -> Fmr
ev = Event_from_json(CGM_399) ev = Event(CGM_399)
self.assertEqual(ev.cgmDataTypeRaw, 1) self.assertEqual(ev.cgmDataTypeRaw, 1)
self.assertEqual(ev.cgmDataType, self.assertEqual(ev.cgmDataType,
eventtypes.LidCgmDataG7.CgmdatatypeBitmask.Fmr) eventtypes.LidCgmDataG7.CgmdatatypeBitmask.Fmr)
def test_multi_bit_array_round_trips_to_int(self): def test_multi_bit_array_round_trips_to_int(self):
# egvInfoBitmask:[0,5,6,7,8,11,12] -> sum(1<<i) == 6625 # egvInfoBitmask:[0,5,6,7,8,11,12] -> sum(1<<i) == 6625
ev = Event_from_json(CGM_399) ev = Event(CGM_399)
self.assertEqual(ev.egvInfoBitmaskRaw, self.assertEqual(ev.egvInfoBitmaskRaw,
sum(1 << i for i in [0, 5, 6, 7, 8, 11, 12])) sum(1 << i for i in [0, 5, 6, 7, 8, 11, 12]))
self.assertEqual(ev.egvInfoBitmaskRaw, 6625) self.assertEqual(ev.egvInfoBitmaskRaw, 6625)
@@ -192,7 +192,7 @@ class TestBitmaskFields(unittest.TestCase):
# An empty array must fold to 0, not None (matches the byte path). # An empty array must fold to 0, not None (matches the byte path).
event = dict(UMC_229) event = dict(UMC_229)
event["eventProperties"] = dict(UMC_229["eventProperties"], activeSleepSchedule=[]) event["eventProperties"] = dict(UMC_229["eventProperties"], activeSleepSchedule=[])
ev = Event_from_json(event) ev = Event(event)
self.assertEqual(ev.activesleepscheduleRaw, 0) self.assertEqual(ev.activesleepscheduleRaw, 0)
@@ -204,14 +204,14 @@ class TestRawFieldShims(unittest.TestCase):
def test_raw_timestamp_shim_available(self): def test_raw_timestamp_shim_available(self):
# process_device_status uses `sorted(events, key=lambda x: x.raw.timestamp)`, # process_device_status uses `sorted(events, key=lambda x: x.raw.timestamp)`,
# so adapted events must expose raw.timestamp as the wall-clock instant. # so adapted events must expose raw.timestamp as the wall-clock instant.
ev = Event_from_json(BASAL_279) ev = Event(BASAL_279)
self.assertEqual(ev.raw.timestamp.format('YYYY-MM-DDTHH:mm:ss'), self.assertEqual(ev.raw.timestamp.format('YYYY-MM-DDTHH:mm:ss'),
"2026-04-30T00:03:29") "2026-04-30T00:03:29")
def test_cgm_egv_timestamp_is_raw_seconds(self): def test_cgm_egv_timestamp_is_raw_seconds(self):
# egvTimeStamp (camelCase in the JSON) normalizes onto egvTimestamp and is # egvTimeStamp (camelCase in the JSON) normalizes onto egvTimestamp and is
# kept as a raw seconds int; ProcessCGMReading adds TANDEM_EPOCH to it. # kept as a raw seconds int; ProcessCGMReading adds TANDEM_EPOCH to it.
ev = Event_from_json(CGM_399) ev = Event(CGM_399)
self.assertEqual(ev.egvTimestamp, 579571288) self.assertEqual(ev.egvTimestamp, 579571288)
self.assertEqual(ev.currentglucosedisplayvalue, 167) self.assertEqual(ev.currentglucosedisplayvalue, 167)
+68 -1
View File
@@ -5,7 +5,7 @@ import arrow
from tconnectsync.sync.tandemsource.process_alarm import ProcessAlarm from tconnectsync.sync.tandemsource.process_alarm import ProcessAlarm
from tconnectsync.eventparser import events as eventtypes 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 ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi from ...nightscout_fake import NightscoutApi
@@ -60,6 +60,73 @@ class TestProcessAlarm(unittest.TestCase):
# Real captured LID_ALARM_ACTIVATED (eventCode 5) events
# (deviceAssignmentId redacted).
ALARM_PUMP_RESET = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 5,
"sequenceGroup": 0,
"sequenceNumber": 2353636,
"pumpDateTime": "2024-02-26T22:44:48",
"estimatedDateTime": "2024-02-26T22:44:48Z",
"eventProperties": {"alarmId": 3, "faultLocatorData": 8230, "param1": 0, "param2": 0},
}
ALARM_EMPTY_CARTRIDGE = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 5,
"sequenceGroup": 0,
"sequenceNumber": 1124751,
"pumpDateTime": "2024-12-23T09:36:37",
"estimatedDateTime": "2024-12-23T09:36:37Z",
"eventProperties": {"alarmId": 8, "faultLocatorData": 8241, "param1": 103, "param2": 9.475377},
}
ALARM_RESUME = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 5,
"sequenceGroup": 0,
"sequenceNumber": 448136,
"pumpDateTime": "2026-05-16T00:06:00",
"estimatedDateTime": "2026-05-16T00:06:00Z",
"eventProperties": {"alarmId": 18, "faultLocatorData": 8311, "param1": 5228339, "param2": 0},
}
class TestProcessAlarmJson(unittest.TestCase):
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.nightscout = NightscoutApi()
self.process = ProcessAlarm(self.tconnect, self.nightscout, 'abcdef', pretend=False)
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
def test_reportable_alarms(self):
p = self.process.process(list(Events([dict(ALARM_PUMP_RESET), dict(ALARM_EMPTY_CARTRIDGE)])), None, None)
self.assertEqual(len(p), 2)
self.assertDictEqual(p[0], {
'eventType': 'Alarm',
'reason': 'PumpResetAlarm',
'notes': 'PumpResetAlarm',
'created_at': '2024-02-26 22:44:48-05:00',
'enteredBy': 'Pump (tconnectsync)',
'pump_event_id': '2353636'
})
self.assertDictEqual(p[1], {
'eventType': 'Alarm',
'reason': 'EmptyCartridgeAlarm',
'notes': 'EmptyCartridgeAlarm',
'created_at': '2024-12-23 09:36:37-05:00',
'enteredBy': 'Pump (tconnectsync)',
'pump_event_id': '1124751'
})
def test_resume_alarm_skipped(self):
p = self.process.process(list(Events([dict(ALARM_RESUME)])), None, None)
self.assertEqual(p, [])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
@@ -5,7 +5,7 @@ import arrow
from tconnectsync.sync.tandemsource.process_cgm_reading import ProcessCGMReading from tconnectsync.sync.tandemsource.process_cgm_reading import ProcessCGMReading
from tconnectsync.eventparser import events as eventtypes from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.generic import Event, Events_from_json from tconnectsync.eventparser.generic import Event, Events
from ...api.fake import TConnectApi from ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi from ...nightscout_fake import NightscoutApi
@@ -421,7 +421,7 @@ class TestProcessCGMReadingFSL3(unittest.TestCase):
# Real LID_CGM_DATA_G7 pump-logs JSON events captured from a live Mobi account # Real LID_CGM_DATA_G7 pump-logs JSON events captured from a live Mobi account
# (deviceAssignmentId redacted). These exercise the production path # (deviceAssignmentId redacted). These exercise the production path
# (Events_from_json -> ProcessCGMReading) rather than the binary decoder. # (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_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_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_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"}
@@ -438,7 +438,7 @@ class TestProcessCGMReadingG7Json(unittest.TestCase):
self.process = ProcessCGMReading(self.tconnect, self.nightscout, 'abcdef', pretend=False, timezone='America/New_York') self.process = ProcessCGMReading(self.tconnect, self.nightscout, 'abcdef', pretend=False, timezone='America/New_York')
def test_single_g7_json_reading(self): def test_single_g7_json_reading(self):
events = list(Events_from_json([dict(G7_JSON_1)])) events = list(Events([dict(G7_JSON_1)]))
self.assertEqual(type(events[0]), eventtypes.LidCgmDataG7) self.assertEqual(type(events[0]), eventtypes.LidCgmDataG7)
self.assertEqual(events[0].currentglucosedisplayvalue, 347) self.assertEqual(events[0].currentglucosedisplayvalue, 347)
self.assertEqual(events[0].egvTimestamp, 580608249) self.assertEqual(events[0].egvTimestamp, 580608249)
@@ -451,7 +451,7 @@ class TestProcessCGMReadingG7Json(unittest.TestCase):
self.assertEqual(p[0]['pump_event_id'], '484329') self.assertEqual(p[0]['pump_event_id'], '484329')
def test_multiple_g7_json_readings(self): def test_multiple_g7_json_readings(self):
events = list(Events_from_json([dict(G7_JSON_1), dict(G7_JSON_2), dict(G7_JSON_3)])) 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) 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['sgv'] for e in p], [347, 345, 341])
self.assertEqual([e['pump_event_id'] for e in p], ['484329', '484338', '484351']) self.assertEqual([e['pump_event_id'] for e in p], ['484329', '484338', '484351'])
@@ -459,7 +459,7 @@ class TestProcessCGMReadingG7Json(unittest.TestCase):
def test_skips_readings_at_or_before_last_upload(self): def test_skips_readings_at_or_before_last_upload(self):
# Only readings strictly after the last Nightscout upload are returned. # 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-26T00:09:08-0400'}
events = list(Events_from_json([dict(G7_JSON_1), dict(G7_JSON_2), dict(G7_JSON_3)])) 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) 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], [341])
@@ -6,7 +6,7 @@ import struct
from tconnectsync.sync.tandemsource.process_device_status import ProcessDeviceStatus from tconnectsync.sync.tandemsource.process_device_status import ProcessDeviceStatus
from tconnectsync.eventparser import events as eventtypes from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.events import UINT16 from tconnectsync.eventparser.events import UINT16
from tconnectsync.eventparser.generic import Event, Event_from_json from tconnectsync.eventparser.generic import Event, Events
from tconnectsync.eventparser.raw_event import RawEvent from tconnectsync.eventparser.raw_event import RawEvent
from ...api.fake import TConnectApi from ...api.fake import TConnectApi
@@ -155,7 +155,7 @@ class TestProcessDeviceStatus(unittest.TestCase):
# than raise. Feed a real non-daily-basal event (eventCode 16). # than raise. Feed a real non-daily-basal event (eventCode 16).
self.nightscout.last_uploaded_devicestatus = lambda *args, **kwargs: None self.nightscout.last_uploaded_devicestatus = lambda *args, **kwargs: None
events = [Event_from_json({ events = [Event({
"eventCode": 16, "eventCode": 16,
"sequenceGroup": 0, "sequenceGroup": 0,
"sequenceNumber": 100, "sequenceNumber": 100,
@@ -5,7 +5,7 @@ import arrow
from tconnectsync.sync.tandemsource.process_user_mode import ProcessUserMode from tconnectsync.sync.tandemsource.process_user_mode import ProcessUserMode
from tconnectsync.eventparser import events as eventtypes 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 ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi from ...nightscout_fake import NightscoutApi
@@ -277,6 +277,95 @@ class TestProcessUserModeExercise(unittest.TestCase):
# Real captured LID_AA_USER_MODE_CHANGE (eventCode 229) events
# (deviceAssignmentId redacted).
SLEEP_START = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 229,
"sequenceGroup": 0,
"sequenceNumber": 456855,
"pumpDateTime": "2026-05-18T10:16:00",
"estimatedDateTime": "2026-05-18T10:16:00Z",
"eventProperties": {
"currentUserMode": 1, "previousUserMode": 0, "requestedAction": 1,
"spareA3": 0, "sleepStartedByGui": 1, "activeSleepSchedule": [0],
"spareB6": 0, "exerciseStoppedByTimer": 0, "exerciseChoice": 0,
"exerciseTime": 0, "eatingSoonStoppedByTimer": 0,
},
}
SLEEP_STOP = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 229,
"sequenceGroup": 0,
"sequenceNumber": 456952,
"pumpDateTime": "2026-05-18T10:19:55",
"estimatedDateTime": "2026-05-18T10:19:55Z",
"eventProperties": {
"currentUserMode": 0, "previousUserMode": 1, "requestedAction": 2,
"spareA3": 0, "sleepStartedByGui": 1, "activeSleepSchedule": [0],
"spareB6": 0, "exerciseStoppedByTimer": 0, "exerciseChoice": 0,
"exerciseTime": 0, "eatingSoonStoppedByTimer": 0,
},
}
EXERCISE_START = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 229,
"sequenceGroup": 0,
"sequenceNumber": 456961,
"pumpDateTime": "2026-05-18T10:20:04",
"estimatedDateTime": "2026-05-18T10:20:04Z",
"eventProperties": {
"currentUserMode": 2, "previousUserMode": 0, "requestedAction": 3,
"spareA3": 0, "sleepStartedByGui": 0, "activeSleepSchedule": [],
"spareB6": 0, "exerciseStoppedByTimer": 0, "exerciseChoice": 0,
"exerciseTime": 0, "eatingSoonStoppedByTimer": 0,
},
}
EXERCISE_STOP = {
"deviceAssignmentId": "00000000-0000-0000-0000-000000000000",
"eventCode": 229,
"sequenceGroup": 0,
"sequenceNumber": 456965,
"pumpDateTime": "2026-05-18T10:20:15",
"estimatedDateTime": "2026-05-18T10:20:15Z",
"eventProperties": {
"currentUserMode": 1, "previousUserMode": 2, "requestedAction": 4,
"spareA3": 0, "sleepStartedByGui": 0, "activeSleepSchedule": [0],
"spareB6": 0, "exerciseStoppedByTimer": 0, "exerciseChoice": 0,
"exerciseTime": 0, "eatingSoonStoppedByTimer": 0,
},
}
class TestProcessUserModeJson(unittest.TestCase):
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.nightscout = NightscoutApi()
self.process = ProcessUserMode(self.tconnect, self.nightscout, 'abcdef', pretend=False)
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
def test_sleep_and_exercise_pairs(self):
events = list(Events([dict(SLEEP_START), dict(SLEEP_STOP), dict(EXERCISE_START), dict(EXERCISE_STOP)]))
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 2)
self.assertEqual(p[0]['eventType'], 'Sleep')
self.assertEqual(p[0]['reason'], 'Sleep (Manual)')
self.assertEqual(p[0]['pump_event_id'], '456855,456952')
self.assertEqual(p[0]['created_at'], '2026-05-18 10:16:00-04:00')
self.assertAlmostEqual(p[0]['duration'], 3.9166666666666665)
self.assertEqual(p[1]['eventType'], 'Exercise')
self.assertEqual(p[1]['pump_event_id'], '456961,456965')
self.assertEqual(p[1]['created_at'], '2026-05-18 10:20:04-04:00')
self.assertAlmostEqual(p[1]['duration'], 0.18333333333333332)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()