diff --git a/tconnectsync/eventparser/build_events.py b/tconnectsync/eventparser/build_events.py index 492b087..08da069 100644 --- a/tconnectsync/eventparser/build_events.py +++ b/tconnectsync/eventparser/build_events.py @@ -1,6 +1,12 @@ +import re + +def _norm(s): + return re.sub(r'[^a-z0-9]', '', s.lower()) + header = '''# THIS FILE IS AUTOGENERATED. DO NOT EDIT. import struct import logging +import re from dataclasses import dataclass from enum import Enum, IntFlag from .raw_event import RawEvent, BaseEvent @@ -9,6 +15,19 @@ logger = logging.getLogger(__name__) EVENT_LEN = 26 +def _norm(s): + return re.sub(r'[^a-z0-9]', '', s.lower()) + +def _bitmask_arr_to_int(v): + # pump-logs bitmask fields arrive as arrays of set-bit indices; convert to the + # int the generated IntFlag / bitmask_to_list expects. Tolerate an int too. + if isinstance(v, (list, tuple)): + r = 0 + for i in v: + r |= (1 << int(i)) + return r + return int(v) if v is not None else 0 + ''' TYPE_TO_STRUCT = { @@ -56,6 +75,14 @@ class {name}(BaseEvent): {build_p2} ) + @staticmethod + def build_from_json(event): + props = {{_norm(k): v for k, v in event.get("eventProperties", {{}}).items()}} + return {name}( + raw = RawEvent.build_from_json(event), +{build_json} + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -130,6 +157,20 @@ def build_decode(event_def): return '\n'.join([f'{" "*8}{f}' for f in p1s]), '\n'.join([f'{" "*12}{f}' for f in p2s]) +def build_json_kwargs(event_def): + lines = [] + for name, field in event_def["data"].items(): + suffix = 'Raw' if "transform" in field and name[-3:] != 'Raw' else '' + attr = f'{fieldNameFormat(name)}{suffix}' + key = _norm(name) + is_bitmask = "transform" in field and any(tx[0] == 'bitmask' for tx in field["transform"]) + if is_bitmask: + lines.append(f'{attr} = _bitmask_arr_to_int(props.get("{key}", 0)),') + else: + lines.append(f'{attr} = props.get("{key}", None),') + return '\n'.join([f'{" "*12}{l}' for l in lines]) + + def build_transform_funcs(event_def): try: from transforms import TRANSFORMS @@ -154,6 +195,7 @@ def build_event(event_id, event_def): fields_dict = build_fields_dict(event_def), build_p1 = build_decode(event_def)[0], build_p2 = build_decode(event_def)[1], + build_json = build_json_kwargs(event_def), transform_funcs = build_transform_funcs(event_def), id = event_id, raw_name = event_def["name"] diff --git a/tconnectsync/eventparser/events.py b/tconnectsync/eventparser/events.py index e567d24..9e3ffe2 100644 --- a/tconnectsync/eventparser/events.py +++ b/tconnectsync/eventparser/events.py @@ -1,6 +1,7 @@ # THIS FILE IS AUTOGENERATED. DO NOT EDIT. import struct import logging +import re from dataclasses import dataclass from enum import Enum, IntFlag from .raw_event import RawEvent, BaseEvent @@ -9,6 +10,19 @@ logger = logging.getLogger(__name__) EVENT_LEN = 26 +def _norm(s): + return re.sub(r'[^a-z0-9]', '', s.lower()) + +def _bitmask_arr_to_int(v): + # pump-logs bitmask fields arrive as arrays of set-bit indices; convert to the + # int the generated IntFlag / bitmask_to_list expects. Tolerate an int too. + if isinstance(v, (list, tuple)): + r = 0 + for i in v: + r |= (1 << int(i)) + return r + return int(v) if v is not None else 0 + UINT8 = '>B' INT8 = '>b' UINT16 = '>H' @@ -76,6 +90,18 @@ class LidBasalRateChange(BaseEvent): changetypeRaw = changetype, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBasalRateChange( + raw = RawEvent.build_from_json(event), + commandedbasalrate = props.get("commandedbasalrate", None), + basebasalrate = props.get("basebasalrate", None), + maxbasalrate = props.get("maxbasalrate", None), + IDP = props.get("idp", None), + changetypeRaw = _bitmask_arr_to_int(props.get("changetype", 0)), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -269,6 +295,17 @@ class LidAlertActivated(BaseEvent): param2 = param2, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAlertActivated( + raw = RawEvent.build_from_json(event), + alertidRaw = props.get("alertid", None), + faultlocatordata = props.get("faultlocatordata", None), + param1 = props.get("param1", None), + param2 = props.get("param2", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -463,6 +500,17 @@ class LidAlarmActivated(BaseEvent): param2 = param2, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAlarmActivated( + raw = RawEvent.build_from_json(event), + alarmidRaw = props.get("alarmid", None), + faultlocatordata = props.get("faultlocatordata", None), + param1 = props.get("param1", None), + param2 = props.get("param2", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -516,6 +564,17 @@ class LidMalfunctionActivated(BaseEvent): param2 = param2, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidMalfunctionActivated( + raw = RawEvent.build_from_json(event), + malfidRaw = props.get("malfid", None), + faultlocatordata = props.get("faultlocatordata", None), + param1 = props.get("param1", None), + param2 = props.get("param2", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -590,6 +649,17 @@ class LidPumpingSuspended(BaseEvent): rpatimeout = rpatimeout, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidPumpingSuspended( + raw = RawEvent.build_from_json(event), + presuspendstate = props.get("presuspendstate", None), + insulinamount = props.get("insulinamount", None), + suspendreasonRaw = props.get("suspendreason", None), + rpatimeout = props.get("rpatimeout", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -637,6 +707,15 @@ class LidPumpingResumed(BaseEvent): insulinamount = insulinamount, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidPumpingResumed( + raw = RawEvent.build_from_json(event), + preresumestate = props.get("preresumestate", None), + insulinamount = props.get("insulinamount", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -685,6 +764,16 @@ class LidTimeChanged(BaseEvent): Rawrtctime = Rawrtctime, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidTimeChanged( + raw = RawEvent.build_from_json(event), + timeprior = props.get("timeprior", None), + timeafter = props.get("timeafter", None), + Rawrtctime = props.get("rawrtctime", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -734,6 +823,16 @@ class LidDateChanged(BaseEvent): Rawrtctime = Rawrtctime, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidDateChanged( + raw = RawEvent.build_from_json(event), + dateprior = props.get("dateprior", None), + dateafter = props.get("dateafter", None), + Rawrtctime = props.get("rawrtctime", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -869,6 +968,21 @@ class LidBgReadingTaken(BaseEvent): cgmcalibrationRaw = cgmcalibration, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBgReadingTaken( + raw = RawEvent.build_from_json(event), + selectediobRaw = props.get("selectediob", None), + BG = props.get("bg", None), + bgentrytypeRaw = props.get("bgentrytype", None), + IOB = props.get("iob", None), + targetbg = props.get("targetbg", None), + ISF = props.get("isf", None), + bgsourcetypeRaw = props.get("bgsourcetype", None), + cgmcalibrationRaw = props.get("cgmcalibration", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -957,6 +1071,18 @@ class LidBolusCompleted(BaseEvent): IOB = IOB, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolusCompleted( + raw = RawEvent.build_from_json(event), + completionstatusRaw = props.get("completionstatus", None), + bolusid = props.get("bolusid", None), + insulindelivered = props.get("insulindelivered", None), + insulinrequested = props.get("insulinrequested", None), + IOB = props.get("iob", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1042,6 +1168,18 @@ class LidBolexCompleted(BaseEvent): IOB = IOB, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolexCompleted( + raw = RawEvent.build_from_json(event), + completionstatusRaw = props.get("completionstatus", None), + bolusid = props.get("bolusid", None), + insulindelivered = props.get("insulindelivered", None), + insulinrequested = props.get("insulinrequested", None), + IOB = props.get("iob", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1229,6 +1367,15 @@ class LidAlertCleared(BaseEvent): faultlocatordata = faultlocatordata, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAlertCleared( + raw = RawEvent.build_from_json(event), + alertidRaw = props.get("alertid", None), + faultlocatordata = props.get("faultlocatordata", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1412,6 +1559,14 @@ class LidAlarmCleared(BaseEvent): alarmidRaw = alarmid, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAlarmCleared( + raw = RawEvent.build_from_json(event), + alarmidRaw = props.get("alarmid", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1456,6 +1611,15 @@ class LidCartridgeFilled(BaseEvent): v2Volume = v2Volume, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCartridgeFilled( + raw = RawEvent.build_from_json(event), + insulinvolume = props.get("insulinvolume", None), + v2Volume = props.get("v2volume", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1513,6 +1677,19 @@ class LidShelfMode(BaseEvent): lipoMv = lipoMv, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidShelfMode( + raw = RawEvent.build_from_json(event), + msecsincereset = props.get("msecsincereset", None), + lipocurrent = props.get("lipocurrent", None), + lipoAbc = props.get("lipoabc", None), + lipoIbc = props.get("lipoibc", None), + lipoRemcap = props.get("liporemcap", None), + lipoMv = props.get("lipomv", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1585,6 +1762,17 @@ class LidBolusActivated(BaseEvent): bolussize = bolussize, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolusActivated( + raw = RawEvent.build_from_json(event), + selectediobRaw = props.get("selectediob", None), + bolusid = props.get("bolusid", None), + IOB = props.get("iob", None), + bolussize = props.get("bolussize", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1655,6 +1843,17 @@ class LidBolexActivated(BaseEvent): bolexsize = bolexsize, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolexActivated( + raw = RawEvent.build_from_json(event), + selectediobRaw = props.get("selectediob", None), + bolusid = props.get("bolusid", None), + IOB = props.get("iob", None), + bolexsize = props.get("bolexsize", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1702,6 +1901,15 @@ class LidDataLogCorruption(BaseEvent): reason = reason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidDataLogCorruption( + raw = RawEvent.build_from_json(event), + block = props.get("block", None), + reason = props.get("reason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1768,6 +1976,15 @@ class LidCannulaFilled(BaseEvent): completionstatusRaw = completionstatus, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCannulaFilled( + raw = RawEvent.build_from_json(event), + primesize = props.get("primesize", None), + completionstatusRaw = props.get("completionstatus", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1837,6 +2054,16 @@ class LidTubingFilled(BaseEvent): position = position, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidTubingFilled( + raw = RawEvent.build_from_json(event), + primesize = props.get("primesize", None), + completionstatusRaw = props.get("completionstatus", None), + position = props.get("position", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -1941,6 +2168,20 @@ class LidBolusRequestedMsg1(BaseEvent): IOB = IOB, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolusRequestedMsg1( + raw = RawEvent.build_from_json(event), + bolusid = props.get("bolusid", None), + bolustypeRaw = props.get("bolustype", None), + correctionbolusincludedRaw = props.get("correctionbolusincluded", None), + carbamount = props.get("carbamount", None), + BG = props.get("bg", None), + carbratioRaw = props.get("carbratio", None), + IOB = props.get("iob", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2095,6 +2336,22 @@ class LidBolusRequestedMsg2(BaseEvent): declinedcorrectionRaw = declinedcorrection, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolusRequestedMsg2( + raw = RawEvent.build_from_json(event), + selectediobRaw = props.get("selectediob", None), + bolusid = props.get("bolusid", None), + optionsRaw = props.get("options", None), + standardpercent = props.get("standardpercent", None), + duration = props.get("duration", None), + ISF = props.get("isf", None), + targetbg = props.get("targetbg", None), + useroverrideRaw = props.get("useroverride", None), + declinedcorrectionRaw = props.get("declinedcorrection", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2153,6 +2410,17 @@ class LidBolusRequestedMsg3(BaseEvent): totalbolussize = totalbolussize, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolusRequestedMsg3( + raw = RawEvent.build_from_json(event), + bolusid = props.get("bolusid", None), + foodbolussize = props.get("foodbolussize", None), + correctionbolussize = props.get("correctionbolussize", None), + totalbolussize = props.get("totalbolussize", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2203,6 +2471,16 @@ class LidNewDay(BaseEvent): featurebitmaskindex = featurebitmaskindex, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidNewDay( + raw = RawEvent.build_from_json(event), + commandedbasalrate = props.get("commandedbasalrate", None), + featuresbitmask = props.get("featuresbitmask", None), + featurebitmaskindex = props.get("featurebitmaskindex", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2255,6 +2533,17 @@ class LidArmInit(BaseEvent): numlogentries = numlogentries, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidArmInit( + raw = RawEvent.build_from_json(event), + version = props.get("version", None), + configabits = props.get("configabits", None), + configbbits = props.get("configbbits", None), + numlogentries = props.get("numlogentries", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2446,6 +2735,21 @@ class LidPlgsPeriodic(BaseEvent): statusRaw = status, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidPlgsPeriodic( + raw = RawEvent.build_from_json(event), + timestamp = props.get("timestamp", None), + FMR = props.get("fmr", None), + PGV = props.get("pgv", None), + fmrstatusRaw = props.get("fmrstatus", None), + pgvvalidRaw = props.get("pgvvalid", None), + rulestateRaw = _bitmask_arr_to_int(props.get("rulestate", 0)), + hominstateRaw = props.get("hominstate", None), + statusRaw = _bitmask_arr_to_int(props.get("status", 0)), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2552,6 +2856,17 @@ class LidCgmAlertActivated(BaseEvent): param2 = param2, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertActivated( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + faultlocatordata = props.get("faultlocatordata", None), + param1 = props.get("param1", None), + param2 = props.get("param2", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2645,6 +2960,14 @@ class LidCgmAlertCleared(BaseEvent): dalertidRaw = dalertid, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertCleared( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2695,6 +3018,17 @@ class LidVersionInfo(BaseEvent): armcrc = armcrc, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidVersionInfo( + raw = RawEvent.build_from_json(event), + version = props.get("version", None), + configabits = props.get("configabits", None), + configbbits = props.get("configbbits", None), + armcrc = props.get("armcrc", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2774,6 +3108,20 @@ class LidUpdateStatus(BaseEvent): swpartnum = swpartnum, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidUpdateStatus( + raw = RawEvent.build_from_json(event), + swupdatestatus = props.get("swupdatestatus", None), + metadataandversionstatus = props.get("metadataandversionstatus", None), + fulldlandcrcstatus = props.get("fulldlandcrcstatus", None), + filedlandsideloadstatus = props.get("filedlandsideloadstatus", None), + externalflashstatus = props.get("externalflashstatus", None), + updatesuccessfulRaw = props.get("updatesuccessful", None), + swpartnum = props.get("swpartnum", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2827,6 +3175,16 @@ class LidCgmStartSessionGx(BaseEvent): sessionduration = sessionduration, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmStartSessionGx( + raw = RawEvent.build_from_json(event), + currenttransmittertime = props.get("currenttransmittertime", None), + sessionstarttime = props.get("sessionstarttime", None), + sessionduration = props.get("sessionduration", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -2918,6 +3276,17 @@ class LidCgmJoinSessionGx(BaseEvent): sessionjoinreasonRaw = sessionjoinreason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmJoinSessionGx( + raw = RawEvent.build_from_json(event), + currenttransmittertime = props.get("currenttransmittertime", None), + sessionstarttime = props.get("sessionstarttime", None), + sessionduration = props.get("sessionduration", None), + sessionjoinreasonRaw = props.get("sessionjoinreason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3013,6 +3382,18 @@ class LidCgmStopSessionGx(BaseEvent): sessionstopreasonRaw = sessionstopreason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmStopSessionGx( + raw = RawEvent.build_from_json(event), + currenttransmittertime = props.get("currenttransmittertime", None), + sessionstarttime = props.get("sessionstarttime", None), + sessionstoptime = props.get("sessionstoptime", None), + sessionduration = props.get("sessionduration", None), + sessionstopreasonRaw = props.get("sessionstopreason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3249,6 +3630,22 @@ class LidAaUserModeChange(BaseEvent): eatingsoonstoppedbytimerRaw = eatingsoonstoppedbytimer, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAaUserModeChange( + raw = RawEvent.build_from_json(event), + exercisechoiceRaw = props.get("exercisechoice", None), + exercisetime = props.get("exercisetime", None), + currentusermodeRaw = props.get("currentusermode", None), + previoususermodeRaw = props.get("previoususermode", None), + requestedactionRaw = props.get("requestedaction", None), + sleepstartedbyguiRaw = props.get("sleepstartedbygui", None), + exercisestoppedbytimerRaw = props.get("exercisestoppedbytimer", None), + activesleepscheduleRaw = _bitmask_arr_to_int(props.get("activesleepschedule", 0)), + eatingsoonstoppedbytimerRaw = props.get("eatingsoonstoppedbytimer", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3449,6 +3846,20 @@ class LidAaPcmChange(BaseEvent): sufficientclosedloopparamsRaw = sufficientclosedloopparams, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAaPcmChange( + raw = RawEvent.build_from_json(event), + currentpcmRaw = props.get("currentpcm", None), + previouspcmRaw = props.get("previouspcm", None), + pumpsuspendedRaw = props.get("pumpsuspended", None), + calculationavailableRaw = props.get("calculationavailable", None), + cgmavailableRaw = props.get("cgmavailable", None), + closedlooppreferredRaw = props.get("closedlooppreferred", None), + sufficientclosedloopparamsRaw = props.get("sufficientclosedloopparams", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3599,6 +4010,22 @@ class LidCgmDataGxb(BaseEvent): interval = interval, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmDataGxb( + raw = RawEvent.build_from_json(event), + glucosevaluestatusRaw = props.get("glucosevaluestatus", None), + cgmDataTypeRaw = _bitmask_arr_to_int(props.get("cgmdatatype", 0)), + rateRaw = props.get("rate", None), + algorithmstate = props.get("algorithmstate", None), + RSSI = props.get("rssi", None), + currentglucosedisplayvalue = props.get("currentglucosedisplayvalue", None), + egvTimestamp = props.get("egvtimestamp", None), + egvInfoBitmaskRaw = _bitmask_arr_to_int(props.get("egvinfobitmask", 0)), + interval = props.get("interval", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3683,6 +4110,18 @@ class LidBasalDelivery(BaseEvent): tempRate = tempRate, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBasalDelivery( + raw = RawEvent.build_from_json(event), + commandedRateSourceRaw = props.get("commandedratesource", None), + commandedRate = props.get("commandedrate", None), + profileBasalRate = props.get("profilebasalrate", None), + algorithmRate = props.get("algorithmrate", None), + tempRate = props.get("temprate", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3828,6 +4267,23 @@ class LidBolusDelivery(BaseEvent): correction = correction, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidBolusDelivery( + raw = RawEvent.build_from_json(event), + bolusid = props.get("bolusid", None), + bolusDeliveryStatusRaw = props.get("bolusdeliverystatus", None), + bolusTypeRaw = _bitmask_arr_to_int(props.get("bolustype", 0)), + bolusSourceRaw = props.get("bolussource", None), + remoteId = props.get("remoteid", None), + requestedNow = props.get("requestednow", None), + requestedLater = props.get("requestedlater", None), + extendedDurationRequested = props.get("extendeddurationrequested", None), + deliveredTotal = props.get("deliveredtotal", None), + correction = props.get("correction", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -3887,6 +4343,17 @@ class LidVersionsA(BaseEvent): bleswversion = bleswversion, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidVersionsA( + raw = RawEvent.build_from_json(event), + armpartnumber = props.get("armpartnumber", None), + armswversion = props.get("armswversion", None), + blepartnumber = props.get("blepartnumber", None), + bleswversion = props.get("bleswversion", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4000,6 +4467,16 @@ class LidAaDailyStatus(BaseEvent): sensortypeRaw = sensortype, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidAaDailyStatus( + raw = RawEvent.build_from_json(event), + pumpcontrolstateRaw = props.get("pumpcontrolstate", None), + usermodeRaw = props.get("usermode", None), + sensortypeRaw = props.get("sensortype", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4124,6 +4601,18 @@ class LidCgmAlertActivatedDex(BaseEvent): param2 = param2, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertActivatedDex( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + sensortypeRaw = props.get("sensortype", None), + faultlocatordata = props.get("faultlocatordata", None), + param1 = props.get("param1", None), + param2 = props.get("param2", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4241,6 +4730,15 @@ class LidCgmAlertClearedDex(BaseEvent): sensortypeRaw = sensortype, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertClearedDex( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + sensortypeRaw = props.get("sensortype", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4376,6 +4874,16 @@ class LidCgmAlertAckDex(BaseEvent): acksourceRaw = acksource, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertAckDex( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + sensortypeRaw = props.get("sensortype", None), + acksourceRaw = props.get("acksource", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4556,6 +5064,22 @@ class LidCgmDataFsl2(BaseEvent): interval = interval, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmDataFsl2( + raw = RawEvent.build_from_json(event), + glucosevaluestatusRaw = props.get("glucosevaluestatus", None), + cgmDataTypeRaw = _bitmask_arr_to_int(props.get("cgmdatatype", 0)), + rateRaw = props.get("rate", None), + algorithmstateRaw = props.get("algorithmstate", None), + RSSI = props.get("rssi", None), + currentglucosedisplayvalue = props.get("currentglucosedisplayvalue", None), + egvTimestamp = props.get("egvtimestamp", None), + egvInfoBitmaskRaw = _bitmask_arr_to_int(props.get("egvinfobitmask", 0)), + interval = props.get("interval", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4608,6 +5132,15 @@ class LidCgmJoinSessionG7(BaseEvent): sessionsignature = sessionsignature, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmJoinSessionG7( + raw = RawEvent.build_from_json(event), + cgmtimestamp = props.get("cgmtimestamp", None), + sessionsignature = props.get("sessionsignature", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4793,6 +5326,22 @@ class LidCgmDataG7(BaseEvent): interval = interval, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmDataG7( + raw = RawEvent.build_from_json(event), + glucosevaluestatusRaw = props.get("glucosevaluestatus", None), + cgmDataTypeRaw = _bitmask_arr_to_int(props.get("cgmdatatype", 0)), + rateRaw = props.get("rate", None), + algorithmstateRaw = props.get("algorithmstate", None), + RSSI = props.get("rssi", None), + currentglucosedisplayvalue = props.get("currentglucosedisplayvalue", None), + egvTimestamp = props.get("egvtimestamp", None), + egvInfoBitmaskRaw = _bitmask_arr_to_int(props.get("egvinfobitmask", 0)), + interval = props.get("interval", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4845,6 +5394,15 @@ class LidCgmStartSessionFsl2(BaseEvent): sessionduration = sessionduration, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmStartSessionFsl2( + raw = RawEvent.build_from_json(event), + sessionstarttime = props.get("sessionstarttime", None), + sessionduration = props.get("sessionduration", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4896,6 +5454,17 @@ class LidCgmStopSessionFsl2(BaseEvent): sessionstopreason = sessionstopreason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmStopSessionFsl2( + raw = RawEvent.build_from_json(event), + sessionstarttime = props.get("sessionstarttime", None), + sessionstoptime = props.get("sessionstoptime", None), + sessionduration = props.get("sessionduration", None), + sessionstopreason = props.get("sessionstopreason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -4949,6 +5518,17 @@ class LidCgmJoinSessionFsl2(BaseEvent): sessionjoinreason = sessionjoinreason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmJoinSessionFsl2( + raw = RawEvent.build_from_json(event), + sessionstarttime = props.get("sessionstarttime", None), + sessionjointime = props.get("sessionjointime", None), + sessionduration = props.get("sessionduration", None), + sessionjoinreason = props.get("sessionjoinreason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5008,6 +5588,19 @@ class LidCgmStopSessionG7(BaseEvent): stopsessioncode = stopsessioncode, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmStopSessionG7( + raw = RawEvent.build_from_json(event), + currenttransmittertime = props.get("currenttransmittertime", None), + sessionstarttime = props.get("sessionstarttime", None), + sessionstoptime = props.get("sessionstoptime", None), + sessionduration = props.get("sessionduration", None), + sessionstopreason = props.get("sessionstopreason", None), + stopsessioncode = props.get("stopsessioncode", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5133,6 +5726,18 @@ class LidCgmAlertActivatedFsl2(BaseEvent): param2 = param2, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertActivatedFsl2( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + sensortypeRaw = props.get("sensortype", None), + faultlocatordata = props.get("faultlocatordata", None), + param1 = props.get("param1", None), + param2 = props.get("param2", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5248,6 +5853,15 @@ class LidCgmAlertClearedFsl2(BaseEvent): sensortypeRaw = sensortype, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmAlertClearedFsl2( + raw = RawEvent.build_from_json(event), + dalertidRaw = props.get("dalertid", None), + sensortypeRaw = props.get("sensortype", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5299,6 +5913,17 @@ class LidCgmJoinSessionFsl3(BaseEvent): sessionjoinreason = sessionjoinreason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmJoinSessionFsl3( + raw = RawEvent.build_from_json(event), + sessionstarttime = props.get("sessionstarttime", None), + sessionjointime = props.get("sessionjointime", None), + sessionduration = props.get("sessionduration", None), + sessionjoinreason = props.get("sessionjoinreason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5464,6 +6089,22 @@ class LidCgmDataFsl3(BaseEvent): interval = interval, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmDataFsl3( + raw = RawEvent.build_from_json(event), + glucosevaluestatusRaw = props.get("glucosevaluestatus", None), + cgmDataTypeRaw = _bitmask_arr_to_int(props.get("cgmdatatype", 0)), + rateRaw = props.get("rate", None), + algorithmstateRaw = props.get("algorithmstate", None), + RSSI = props.get("rssi", None), + currentglucosedisplayvalue = props.get("currentglucosedisplayvalue", None), + egvTimestamp = props.get("egvtimestamp", None), + egvInfoBitmaskRaw = _bitmask_arr_to_int(props.get("egvinfobitmask", 0)), + interval = props.get("interval", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5522,6 +6163,17 @@ class LidCgmStopSessionFsl3(BaseEvent): sessionstopreason = sessionstopreason, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCgmStopSessionFsl3( + raw = RawEvent.build_from_json(event), + sessionstarttime = props.get("sessionstarttime", None), + sessionstoptime = props.get("sessionstoptime", None), + sessionduration = props.get("sessionduration", None), + sessionstopreason = props.get("sessionstopreason", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5584,6 +6236,19 @@ class LidDailyBasal(BaseEvent): batterylipomillivolts = batterylipomillivolts, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidDailyBasal( + raw = RawEvent.build_from_json(event), + dailytotalbasal = props.get("dailytotalbasal", None), + lastbasalrate = props.get("lastbasalrate", None), + iob = props.get("iob", None), + batterychargepercentmsbRaw = props.get("batterychargepercentmsbraw", None), + batterychargepercentlsbRaw = props.get("batterychargepercentlsbraw", None), + batterylipomillivolts = props.get("batterylipomillivolts", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5630,6 +6295,14 @@ class LidCarbsEntered(BaseEvent): carbs = carbs, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidCarbsEntered( + raw = RawEvent.build_from_json(event), + carbs = props.get("carbs", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5671,6 +6344,14 @@ class LidUsbConnected(BaseEvent): negotiatedcurrent = negotiatedcurrent, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidUsbConnected( + raw = RawEvent.build_from_json(event), + negotiatedcurrent = props.get("negotiatedcurrent", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp @@ -5712,6 +6393,14 @@ class LidUsbDisconnected(BaseEvent): negotiatedcurrent = negotiatedcurrent, ) + @staticmethod + def build_from_json(event): + props = {_norm(k): v for k, v in event.get("eventProperties", {}).items()} + return LidUsbDisconnected( + raw = RawEvent.build_from_json(event), + negotiatedcurrent = props.get("negotiatedcurrent", None), + ) + @property def eventTimestamp(self): return self.raw.timestamp diff --git a/tconnectsync/eventparser/generic.py b/tconnectsync/eventparser/generic.py index 176a1ca..7bb66bc 100644 --- a/tconnectsync/eventparser/generic.py +++ b/tconnectsync/eventparser/generic.py @@ -25,4 +25,19 @@ def Event(x): Events = lambda x: (Event(bytearray(e)) for e in batched(x, EVENT_LEN)) def decode_raw_events(raw): - return base64.b64decode(raw) \ No newline at end of file + 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) \ No newline at end of file diff --git a/tconnectsync/eventparser/raw_event.py b/tconnectsync/eventparser/raw_event.py index df1837e..acf3ca0 100644 --- a/tconnectsync/eventparser/raw_event.py +++ b/tconnectsync/eventparser/raw_event.py @@ -48,6 +48,22 @@ class RawEvent: raw = raw ) + @staticmethod + def build_from_json(event): + # pump-logs JSON events carry pumpDateTime (naive local wall-clock, + # no tz). Reproduce the byte path: store timestampRaw as seconds since + # TANDEM_EPOCH parsed AS IF UTC, so the .timestamp property re-forces + # the same wall-clock into TIMEZONE_NAME. source is unused; raw bytes + # are absent on the JSON path. + timestampRaw = arrow.get(event["pumpDateTime"]).int_timestamp - TANDEM_EPOCH + return RawEvent( + source = 0, + id = event["eventCode"], + timestampRaw = timestampRaw, + seqNum = event["sequenceNumber"], + raw = b'' + ) + @property def timestamp(self): # Event timestamps do not have TZ data attached to them when parsed, diff --git a/tests/eventparser/__init__.py b/tests/eventparser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/eventparser/test_from_json.py b/tests/eventparser/test_from_json.py new file mode 100644 index 0000000..6b5bb10 --- /dev/null +++ b/tests/eventparser/test_from_json.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import unittest + +from tconnectsync.eventparser.generic import Event_from_json, Events_from_json +from tconnectsync.eventparser import events as eventtypes +from tconnectsync.eventparser.raw_event import RawEvent + +# Trimmed real pump-log events (values from a captured account response). +BASAL_279 = { + "deviceAssignmentId": "4ff6bebc-d4d6-4423-b123-eecfcf5a4238", + "eventCode": 279, + "sequenceGroup": 0, + "sequenceNumber": 393131, + "pumpDateTime": "2026-04-30T00:03:29", + "eventProperties": { + "commandedRateSource": 3, "reservedA2": 0, "spareA3": 0, + "commandedRate": 0, "profileBasalRate": 1000, "algorithmRate": 0, + "tempRate": 65535, + }, + "estimatedDateTime": "2026-04-30T00:03:29Z", +} + +ALARM_5 = { + "deviceAssignmentId": "4ff6bebc-d4d6-4423-b123-eecfcf5a4238", + "eventCode": 5, + "sequenceGroup": 0, + "sequenceNumber": 500001, + "pumpDateTime": "2026-04-30T01:00:00", + "eventProperties": {"alarmId": 18, "faultLocatorData": 8311, "param1": 3993668, "param2": 0}, + "estimatedDateTime": "2026-04-30T01:00:00Z", +} + + +class TestBuildFromJson(unittest.TestCase): + maxDiff = None + + def test_dispatches_to_correct_class(self): + self.assertIsInstance(Event_from_json(BASAL_279), eventtypes.LidBasalDelivery) + self.assertIsInstance(Event_from_json(ALARM_5), eventtypes.LidAlarmActivated) + + def test_plain_fields(self): + ev = Event_from_json(BASAL_279) + self.assertEqual(ev.commandedRate, 0) + self.assertEqual(ev.profileBasalRate, 1000) + self.assertEqual(ev.tempRate, 65535) + + def test_envelope_fields(self): + ev = Event_from_json(BASAL_279) + self.assertEqual(ev.seqNum, 393131) + self.assertEqual(ev.eventId, 279) + + def test_timestamp_preserves_wall_clock(self): + # eventTimestamp keeps pumpDateTime's wall-clock (tz forced to the + # configured TIMEZONE_NAME), so the naive portion round-trips exactly. + ev = Event_from_json(BASAL_279) + self.assertEqual(ev.eventTimestamp.format('YYYY-MM-DDTHH:mm:ss'), "2026-04-30T00:03:29") + + def test_missing_plain_field_defaults_to_none(self): + event = dict(BASAL_279) + event["eventProperties"] = {k: v for k, v in BASAL_279["eventProperties"].items() if k != "tempRate"} + ev = Event_from_json(event) + self.assertIsNone(ev.tempRate) + self.assertEqual(ev.commandedRate, 0) # others still parse + + def test_extra_keys_are_ignored(self): + event = dict(BASAL_279) + event["eventProperties"] = dict(BASAL_279["eventProperties"], someFutureField=42) + ev = Event_from_json(event) # must not raise + self.assertFalse(hasattr(ev, "someFutureField")) + + def test_events_from_json_yields_in_order(self): + out = list(Events_from_json([BASAL_279, ALARM_5])) + self.assertEqual([type(e).__name__ for e in out], + ["LidBasalDelivery", "LidAlarmActivated"]) + + def test_unknown_eventcode_falls_back_to_rawevent(self): + ev = Event_from_json({ + "eventCode": 99999, + "sequenceNumber": 7, + "pumpDateTime": "2026-04-30T00:00:00", + "eventProperties": {}, + }) + self.assertIs(type(ev), RawEvent) + self.assertEqual(ev.eventId, 99999) + self.assertEqual(ev.seqNum, 7) + self.assertEqual(ev.eventTimestamp.format('YYYY-MM-DDTHH:mm:ss'), "2026-04-30T00:00:00") + + +if __name__ == "__main__": + unittest.main()