mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-24 03:34:12 -05:00
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:
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user