V2 substantial completion

This commit is contained in:
James Woglom
2024-09-29 21:50:17 -04:00
parent c0cddb80b9
commit 210bcddbd3
17 changed files with 1011 additions and 45 deletions
@@ -9,18 +9,27 @@ class EventClass(set, Enum):
BASAL_SUSPENSION = {events.LidPumpingSuspended}
BASAL_RESUME = {events.LidPumpingResumed}
ALARM = {events.LidAlarmActivated, events.LidMalfunctionActivated}
BOLUS = {events.LidBolusCompleted, events.LidBolexCompleted}
BOLUS = {
events.LidBolusRequestedMsg1, # carb amount, bg, iob
events.LidBolusRequestedMsg2, # more robust bolus type
events.LidBolusRequestedMsg3, # total bolus requested amount
events.LidBolusCompleted, # final event showing amount delivered
events.LidBolexCompleted # extended bolus
}
CARTRIDGE = {events.LidCartridgeFilled, events.LidCannulaFilled, events.LidTubingFilled}
CGM_ALERT = {events.LidCgmAlertActivated, events.LidCgmAlertActivatedDex, events.LidCgmAlertActivatedFsl2}
CGM_START = {events.LidCgmStartSessionGx, events.LidCgmStartSessionFsl2}
CGM_JOIN = {events.LidCgmJoinSessionGx, events.LidCgmJoinSessionG7, events.LidCgmJoinSessionFsl2}
CGM_STOP = {events.LidCgmStopSessionGx, events.LidCgmStopSessionG7, events.LidCgmStopSessionFsl2}
_CGM_START = {events.LidCgmStartSessionGx, events.LidCgmStartSessionFsl2}
_CGM_JOIN = {events.LidCgmJoinSessionGx, events.LidCgmJoinSessionG7, events.LidCgmJoinSessionFsl2}
_CGM_STOP = {events.LidCgmStopSessionGx, events.LidCgmStopSessionG7, events.LidCgmStopSessionFsl2}
CGM_START_JOIN_STOP = {*_CGM_START, *_CGM_JOIN, *_CGM_STOP}
CGM_READING = {events.LidCgmDataGxb, events.LidCgmDataG7, events.LidCgmDataFsl2}
USER_MODE = {events.LidAaUserModeChange}
@staticmethod
def for_event(evt):
for typ, vals in EventClass.__members__.items():
if typ.startswith('_'):
continue
if type(evt) == type and evt in vals:
return EventClass.__members__[typ]
elif type(evt) in vals:
+161 -7
View File
@@ -1935,7 +1935,29 @@ class LidCgmAlertActivated(BaseEvent):
param1: int
param2: float
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
@staticmethod
def build(raw):
dalertid, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 10)
@@ -1969,7 +1991,29 @@ class LidCgmAlertCleared(BaseEvent):
raw: RawEvent
dalertidRaw: int
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
@staticmethod
def build(raw):
dalertid, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 10)
@@ -3004,7 +3048,29 @@ class LidCgmAlertActivatedDex(BaseEvent):
param1: int
param2: float
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
SensortypeMap = {
"0": "Invalid",
"1": "CGM_TYPE_DEXCOM_G6",
@@ -3056,7 +3122,29 @@ class LidCgmAlertClearedDex(BaseEvent):
dalertidRaw: int
sensortypeRaw: int
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
SensortypeMap = {
"0": "Invalid",
"1": "CGM_TYPE_DEXCOM_G6",
@@ -3103,7 +3191,29 @@ class LidCgmAlertAckDex(BaseEvent):
sensortypeRaw: int
acksourceRaw: int
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
SensortypeMap = {
"0": "Invalid",
"1": "CGM_TYPE_DEXCOM_G6",
@@ -3642,7 +3752,29 @@ class LidCgmAlertActivatedFsl2(BaseEvent):
param1: int
param2: float
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
SensortypeMap = {
"0": "Invalid",
"2": "CGM_TYPE_LIBRE2"
@@ -3692,7 +3824,29 @@ class LidCgmAlertClearedFsl2(BaseEvent):
dalertidRaw: int
sensortypeRaw: int
# Dictionary unknown: dalerts
DalertidMap = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
class DalertidEnum(Enum):
CgmSensorFail = 11
CgmSensorExpired = 13
CgmOutOfRange = 14
CgmTransmitterError = 20
CgmTemperature = 26
CgmFailedConnection = 27
CgmTransmitterExpired = 39
@property
def dalertid(self):
return self.DalertidEnum(self.dalertidRaw)
SensortypeMap = {
"0": "Invalid",
"2": "CGM_TYPE_LIBRE2"
+10
View File
@@ -130,4 +130,14 @@ ALARMS_DICT = {
"61": "DEFAULT_ALARM_61",
"62": "DEFAULT_ALARM_62",
"63": "DEFAULT_ALARM_63",
}
CGM_ALERTS_DICT = {
"11": "CGM Sensor Fail",
"13": "CGM Sensor Expired",
"14": "CGM Out Of Range",
"20": "CGM Transmitter Error",
"26": "CGM Temperature",
"27": "CGM Failed Connection",
"39": "CGM Transmitter Expired"
}
+5 -2
View File
@@ -1,8 +1,8 @@
import json
try:
from static_dicts import ALERTS_DICT, ALARMS_DICT
from static_dicts import ALERTS_DICT, ALARMS_DICT, CGM_ALERTS_DICT
except ImportError:
from .static_dicts import ALERTS_DICT, ALARMS_DICT
from .static_dicts import ALERTS_DICT, ALARMS_DICT, CGM_ALERTS_DICT
def enumNameFormat(text):
if not text:
@@ -65,6 +65,9 @@ def transform_dictionary(event_def, name, name_fmt, field, tx):
if tx == 'alarms':
return transform_enum(event_def, name, name_fmt, field, ALARMS_DICT)
if tx == 'dalerts':
return transform_enum(event_def, name, name_fmt, field, CGM_ALERTS_DICT)
return [f'# Dictionary unknown: {tx}']
def transform_bitmask(event_def, name, name_fmt, field, tx):
+5 -3
View File
@@ -9,6 +9,7 @@ CGM = "CGM"
PUMP_EVENTS = "PUMP_EVENTS"
PUMP_EVENTS_BASAL_SUSPENSION = "PUMP_EVENTS_BASAL_SUSPENSION"
PROFILES = "PROFILES"
CGM_ALERTS = "CGM_ALERTS"
DEFAULT_FEATURES = [
BASAL,
@@ -22,13 +23,14 @@ ALL_FEATURES = [
IOB,
PUMP_EVENTS,
PUMP_EVENTS_BASAL_SUSPENSION,
PROFILES
PROFILES,
CGM,
CGM_ALERTS,
]
# These modes are not yet ready for wide use.
if ENABLE_TESTING_MODES:
ALL_FEATURES += [
BOLUS_BG,
CGM
BOLUS_BG
]
+80 -20
View File
@@ -14,6 +14,10 @@ ACTIVITY_EVENTTYPE = "Activity"
EXERCISE_EVENTTYPE = "Exercise"
SLEEP_EVENTTYPE = "Sleep"
ALARM_EVENTTYPE = "Alarm"
CGM_ALERT_EVENTTYPE = "CGM Alert"
CGM_START_EVENTTYPE = "CGM Start Session"
CGM_JOIN_EVENTTYPE = "CGM Join Session"
CGM_STOP_EVENTTYPE = "CGM Stop Session"
IOB_ACTIVITYTYPE = "tconnect_iob"
@@ -23,7 +27,7 @@ Conversion methods for parsing data into Nightscout objects.
"""
class NightscoutEntry:
@staticmethod
def basal(value, duration_mins, created_at, reason=""):
def basal(value, duration_mins, created_at, reason="", pump_event_id=""):
return {
"eventType": BASAL_EVENTTYPE,
"reason": reason,
@@ -33,7 +37,8 @@ class NightscoutEntry:
"created_at": created_at,
"carbs": None,
"insulin": None,
"enteredBy": ENTERED_BY
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
# Note that Nightscout is not consistent and uses "Sensor"/"Finger"
@@ -46,19 +51,24 @@ class NightscoutEntry:
data = {
"eventType": BOLUS_EVENTTYPE,
"created_at": created_at,
"carbs": int(carbs),
"carbs": int(carbs) if carbs else None,
"insulin": float(bolus),
"notes": notes,
"enteredBy": ENTERED_BY,
}
if bg:
if bg_type not in (NightscoutEntry.SENSOR, NightscoutEntry.FINGER):
raise InvalidBolusTypeException("bg_type: %s (%s)" % (bg_type, data))
if bg_type:
if bg_type not in (NightscoutEntry.SENSOR, NightscoutEntry.FINGER):
raise InvalidBolusTypeException("bg_type: %s (%s)" % (bg_type, data))
data.update({
"glucose": str(bg),
"glucoseType": bg_type
})
data.update({
"glucose": str(bg),
"glucoseType": bg_type
})
else:
data.update({
"glucose": str(bg)
})
return data
@staticmethod
@@ -71,65 +81,115 @@ class NightscoutEntry:
}
@staticmethod
def entry(sgv, created_at):
def entry(sgv, created_at, pump_event_id=""):
return {
"type": "sgv",
"sgv": int(sgv),
"date": int(1000 * arrow.get(created_at).timestamp()),
"dateString": arrow.get(created_at).strftime('%Y-%m-%dT%H:%M:%S%z'),
"device": ENTERED_BY,
"pump_event_id": pump_event_id,
# delta, direction are undefined
}
@staticmethod
def sitechange(created_at, reason=""):
def sitechange(created_at, reason="", pump_event_id=""):
return {
"eventType": SITECHANGE_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def basalsuspension(created_at, reason=""):
def basalsuspension(created_at, reason="", pump_event_id=""):
return {
"eventType": BASALSUSPENSION_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def basalresume(created_at):
def basalresume(created_at, pump_event_id=""):
return {
"eventType": BASALRESUME_EVENTTYPE,
"reason": "Basal resumed",
"notes": "Basal resumed",
"created_at": created_at,
"enteredBy": ENTERED_BY
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def alarm(created_at, reason=""):
def alarm(created_at, reason="", pump_event_id=""):
return {
"eventType": ALARM_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def activity(created_at, duration, reason="", event_type=ACTIVITY_EVENTTYPE):
def cgm_alert(created_at, reason="", pump_event_id=""):
return {
"eventType": CGM_ALERT_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def cgm_start(created_at, reason="", pump_event_id=""):
return {
"eventType": CGM_START_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def cgm_join(created_at, reason="", pump_event_id=""):
return {
"eventType": CGM_JOIN_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def cgm_stop(created_at, reason="", pump_event_id=""):
return {
"eventType": CGM_STOP_EVENTTYPE,
"reason": reason,
"notes": reason,
"created_at": created_at,
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
@staticmethod
def activity(created_at, duration, reason="", event_type=ACTIVITY_EVENTTYPE, pump_event_id=""):
return {
"eventType": event_type,
"reason": reason,
"notes": reason,
"duration": float(duration),
"created_at": created_at,
"enteredBy": ENTERED_BY
"enteredBy": ENTERED_BY,
"pump_event_id": pump_event_id
}
# Tandem-scraped profile to Nightscout profile store entry
+19 -3
View File
@@ -9,6 +9,12 @@ from .process_basal import ProcessBasal
from .process_basal_suspension import ProcessBasalSuspension
from .process_basal_resume import ProcessBasalResume
from .process_alarm import ProcessAlarm
from .process_bolus import ProcessBolus
from .process_cartridge import ProcessCartridge
from .process_cgm_alert import ProcessCGMAlert
from .process_cgm_start_join_stop import ProcessCGMStartJoinStop
from .process_cgm_reading import ProcessCGMReading
from .process_user_mode import ProcessUserMode
logger = logging.getLogger(__name__)
@@ -25,7 +31,13 @@ class ProcessTimeRange:
EventClass.BASAL.name: ProcessBasal,
EventClass.BASAL_SUSPENSION.name: ProcessBasalSuspension,
EventClass.BASAL_RESUME.name: ProcessBasalResume,
EventClass.ALARM.name: ProcessAlarm
EventClass.ALARM.name: ProcessAlarm,
EventClass.BOLUS.name: ProcessBolus,
EventClass.CARTRIDGE.name: ProcessCartridge,
EventClass.CGM_ALERT.name: ProcessCGMAlert,
EventClass.CGM_START_JOIN_STOP.name: ProcessCGMStartJoinStop,
EventClass.CGM_READING.name: ProcessCGMReading,
EventClass.USER_MODE.name: ProcessUserMode,
}
def process(self, time_start, time_end):
@@ -59,8 +71,12 @@ class ProcessTimeRange:
for clazz, events in for_eventclass.items():
if clazz in self.event_classes.keys():
c = self.event_classes[clazz](self.tconnect, self.nightscout, self.tconnect_device_id, self.pretend, self.features)
ns_entries = c.process(events, events_first_time, events_last_time)
processed_count += c.write(ns_entries)
if c.enabled():
logger.info("%s is enabled from features %s" % (clazz, self.features))
ns_entries = c.process(events, events_first_time, events_last_time)
processed_count += c.write(ns_entries)
else:
logger.info("Skipping %s, is not enabled from features %s" % (clazz, self.features))
return processed_count
@@ -2,6 +2,7 @@ import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
@@ -21,6 +22,9 @@ class ProcessAlarm:
self.pretend = pretend
self.features = features
def enabled(self):
return features.PUMP_EVENTS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessAlarm: querying for last uploaded alarm")
last_upload = self.nightscout.last_uploaded_entry(ALARM_EVENTTYPE, time_start=time_start, time_end=time_end)
@@ -58,10 +62,12 @@ class ProcessAlarm:
if type(event) == eventtypes.LidAlarmActivated:
return NightscoutEntry.alarm(
created_at = event.eventTimestamp,
reason = event.alarmid
reason = event.alarmid,
pump_event_id = event.eventId
)
elif type(event) == eventtypes.LidMalfunctionActivated:
return NightscoutEntry.alarm(
created_at = event.eventTimestamp,
reason = "Malfunction"
reason = "Malfunction",
pump_event_id = event.eventId
)
@@ -2,6 +2,7 @@ import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
@@ -21,6 +22,9 @@ class ProcessBasal:
self.pretend = pretend
self.features = features
def enabled(self):
return features.BASAL in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessBasal: querying for last uploaded entry")
last_upload = self.nightscout.last_uploaded_entry(BASAL_EVENTTYPE, time_start=time_start, time_end=time_end)
@@ -68,12 +72,14 @@ class ProcessBasal:
value = event.commandedbasalrate,
duration_mins = duration.seconds / 60,
created_at = start,
reason = ', '.join(bitmask_to_list(event.changetype))
reason = ', '.join(bitmask_to_list(event.changetype)),
pump_event_id = event.eventId
)
if type(event) == eventtypes.LidBasalDelivery:
return NightscoutEntry.basal(
value = event.commandedRate,
duration_mins = duration.seconds / 60,
created_at = start,
reason = ', '.join(bitmask_to_list(event.commandedRateSource))
reason = ', '.join(bitmask_to_list(event.commandedRateSource)),
pump_event_id = event.eventId
)
@@ -2,6 +2,7 @@ import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
@@ -21,6 +22,9 @@ class ProcessBasalResume:
self.pretend = pretend
self.features = features
def enabled(self):
return features.PUMP_EVENTS in self.features or features.BASAL in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessBasalResume: querying for last uploaded resume-suspension")
last_upload = self.nightscout.last_uploaded_entry(BASALRESUME_EVENTTYPE, time_start=time_start, time_end=time_end)
@@ -57,5 +61,6 @@ class ProcessBasalResume:
def resume_to_nsentry(self, event):
if type(event) == eventtypes.LidPumpingResumed:
return NightscoutEntry.basalresume(
created_at = event.eventTimestamp
created_at = event.eventTimestamp,
pump_event_id = event.eventId
)
@@ -2,6 +2,7 @@ import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
@@ -21,6 +22,9 @@ class ProcessBasalSuspension:
self.pretend = pretend
self.features = features
def enabled(self):
return features.PUMP_EVENTS in self.features or features.BASAL in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessBasalSuspension: querying for last uploaded suspension")
last_upload = self.nightscout.last_uploaded_entry(BASALSUSPENSION_EVENTTYPE, time_start=time_start, time_end=time_end)
@@ -58,5 +62,6 @@ class ProcessBasalSuspension:
if type(event) == eventtypes.LidPumpingSuspended:
return NightscoutEntry.basalsuspension(
created_at = event.eventTimestamp,
reason = ', '.join(bitmask_to_list(event.suspendreason))
reason = ', '.join(bitmask_to_list(event.suspendreason)),
pump_event_id = event.eventId
)
@@ -0,0 +1,106 @@
import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from ...parser.nightscout import (
BOLUS_EVENTTYPE,
NightscoutEntry
)
logger = logging.getLogger(__name__)
class ProcessBolus:
def __init__(self, tconnect, nightscout, tconnect_device_id, pretend, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnect_device_id
self.pretend = pretend
self.features = features
def enabled(self):
return features.BOLUS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessBolus: querying for last uploaded entry")
last_upload = self.nightscout.last_uploaded_entry(BOLUS_EVENTTYPE, time_start=time_start, time_end=time_end)
last_upload_time = None
if last_upload:
last_upload_time = arrow.get(last_upload["created_at"])
logger.info("Last Nightscout bolus upload: %s" % last_upload_time)
# TODO EXTENDED BOLUSES
bolusCompletedEvents = []
bolusEventsForId = {}
for event in sorted(events, key=lambda x: x.eventTimestamp):
if event.bolusid not in bolusEventsForId.keys():
bolusEventsForId[event.bolusid] = {}
bolusEventsForId[event.bolusid][type(event)] = event
if type(event) == eventtypes.LidBolusCompleted:
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if self.pretend:
logger.info("Skipping bolusCompletedEvent before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
bolusCompletedEvents.append(event)
bolusCompletedEvents.sort(lambda e: e.eventTimestamp)
ns_entries = []
for bolusCompleted in bolusCompletedEvents:
m = bolusEventsForId[bolusCompleted.bolusid]
ns_entries.append(self.bolus_to_nsentry(
bolusCompleted,
bolusRequested1 = m.get(events.LidBolusRequestedMsg1),
bolusRequested2 = m.get(events.LidBolusRequestedMsg2),
bolusRequested3 = m.get(events.LidBolusRequestedMsg3),
))
return ns_entries
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload to Nightscout: %s" % entry)
else:
logger.info("Uploading to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry)
count += 1
return count
def bolus_to_nsentry(self, bolusCompleted, bolusRequested1, bolusRequested2, bolusRequested3):
suffixes = []
if bolusRequested2 and bolusRequested2.useroverride == eventtypes.LidBolusRequestedMsg2.UseroverrideEnum.Yes:
suffixes.append('(Override)')
if bolusRequested2 and bolusRequested2.declinedcorrection == eventtypes.LidBolusRequestedMsg2.DeclinedcorrectionEnum.Yes:
suffixes.append('(Declined Correction)')
suffix = (' ' + (' '.join(suffixes))) if suffixes else ''
event_ids = []
for e in [bolusCompleted, bolusRequested1, bolusRequested2, bolusRequested3]:
if e:
event_ids.append(e.eventId)
return NightscoutEntry.bolus(
bolus = bolusCompleted.insulindelivered,
carbs = bolusRequested1.carbamount if bolusRequested1 and bolusRequested1.carbamount>0 else None,
created_at = bolusCompleted.eventTimestamp,
notes = (eventtypes.LidBolusRequestedMsg2.OptionsMap[bolusRequested2.optionsRaw] if bolusRequested2 else '') + suffix,
bg = bolusRequested1.BG if bolusRequested1 and bolusRequested1.BG > 0 else None,
pump_event_id = ",".join(event_ids)
)
@@ -0,0 +1,100 @@
import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from ...parser.nightscout import (
SITECHANGE_EVENTTYPE,
NightscoutEntry
)
logger = logging.getLogger(__name__)
class ProcessCartridge:
def __init__(self, tconnect, nightscout, tconnect_device_id, pretend, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnect_device_id
self.pretend = pretend
self.features = features
def enabled(self):
return features.PUMP_EVENTS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessCartridge: querying for last uploaded entry")
last_upload = self.nightscout.last_uploaded_entry(SITECHANGE_EVENTTYPE, time_start=time_start, time_end=time_end)
last_upload_time = None
if last_upload:
last_upload_time = arrow.get(last_upload["created_at"])
logger.info("Last Nightscout sitechange upload: %s" % last_upload_time)
cartFilledEvents = []
cannulaFilledEvents = []
tubingFilledEvents = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if self.pretend:
logger.info("Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
if type(event) == eventtypes.LidCartridgeFilled:
cartFilledEvents.append(event)
elif type(event) == eventtypes.LidCannulaFilled:
cannulaFilledEvents.append(event)
elif type(event) == eventtypes.LidTubingFilled:
tubingFilledEvents.append(event)
cartFilledEvents.sort(lambda e: e.eventTimestamp)
cannulaFilledEvents.sort(lambda e: e.eventTimestamp)
tubingFilledEvents.sort(lambda e: e.eventTimestamp)
ns_entries = []
for cartFilled in cartFilledEvents:
ns_entries.append(self.cart_to_nsentry(cartFilled))
for cannulaFilled in cannulaFilledEvents:
ns_entries.append(self.cannula_to_nsentry(cannulaFilled))
for tubingFilled in tubingFilledEvents:
ns_entries.append(self.tubing_to_nsentry(tubingFilled))
return ns_entries
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload to Nightscout: %s" % entry)
else:
logger.info("Uploading to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry)
count += 1
return count
def cart_to_nsentry(self, cartFilled):
return NightscoutEntry.sitechange(
created_at = cartFilled.eventTimestamp,
reason = "Cartridge Filled" + (" (%du filled)" % round(cartFilled.v2Volume) if cartFilled.v2Volume else ""),
pump_event_id = cartFilled.eventId
)
def cannula_to_nsentry(self, cannulaFilled):
return NightscoutEntry.sitechange(
created_at = cannulaFilled.eventTimestamp,
reason = "Cannula Filled" + (" (%du primed)" % round(cannulaFilled.primesize) if cannulaFilled.primesize else ""),
pump_event_id = cannulaFilled.eventId
)
def tubing_to_nsentry(self, tubingFilled):
return NightscoutEntry.sitechange(
created_at = tubingFilled.eventTimestamp,
reason = "Tubing Filled" + (" (%du primed)" % round(tubingFilled.primesize) if tubingFilled.primesize else ""),
pump_event_id = tubingFilled.eventId
)
@@ -0,0 +1,83 @@
import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from ...parser.nightscout import (
CGM_ALERT_EVENTTYPE,
NightscoutEntry
)
logger = logging.getLogger(__name__)
class ProcessCGMAlert:
def __init__(self, tconnect, nightscout, tconnect_device_id, pretend, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnect_device_id
self.pretend = pretend
self.features = features
def enabled(self):
return features.CGM_ALERTS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessCGMAlert: querying for last uploaded entry")
last_upload = self.nightscout.last_uploaded_entry(CGM_ALERT_EVENTTYPE, time_start=time_start, time_end=time_end)
last_upload_time = None
if last_upload:
last_upload_time = arrow.get(last_upload["created_at"])
logger.info("Last Nightscout cgmalert upload: %s" % last_upload_time)
alertEvents = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if self.pretend:
logger.info("Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
alertEvents.append(event)
alertEvents.sort(lambda e: e.eventTimestamp)
ns_entries = []
for event in alertEvents:
ns_entries.append(self.alert_to_nsentry(event))
return ns_entries
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload to Nightscout: %s" % entry)
else:
logger.info("Uploading to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry)
count += 1
return count
def alert_to_nsentry(self, alert):
if type(alert) == eventtypes.LidCgmAlertActivated:
return NightscoutEntry.cgm_alert(
created_at = alert.eventTimestamp,
reason = ("CGM Alert (%s)" % alert.dalertid) if alert.dalertid else "CGM Alert (Unknown)",
pump_event_id = alert.eventId
)
elif type(alert) == eventtypes.LidCgmAlertActivatedDex:
return NightscoutEntry.cgm_alert(
created_at = alert.eventTimestamp,
reason = ("Dexcom CGM Alert (%s)" % alert.dalertid) if alert.dalertid else "Dexcom CGM Alert (Unknown)",
pump_event_id = alert.eventId
)
elif type(alert) == eventtypes.LidCgmAlertActivatedFsl2:
return NightscoutEntry.cgm_alert(
created_at = alert.eventTimestamp,
reason = ("Libre CGM Alert (%s)" % alert.dalertid) if alert.dalertid else "Libre CGM Alert (Unknown)",
pump_event_id = alert.eventId
)
@@ -0,0 +1,74 @@
import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser.raw_event import TANDEM_EPOCH
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from ...parser.nightscout import (
CGM_START_EVENTTYPE,
NightscoutEntry
)
logger = logging.getLogger(__name__)
class ProcessCGMReading:
def __init__(self, tconnect, nightscout, tconnect_device_id, pretend, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnect_device_id
self.pretend = pretend
self.features = features
def enabled(self):
return features.CGM in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessCGMReading: querying for last uploaded entry")
last_upload = self.nightscout.last_uploaded_bg_entry(time_start=time_start, time_end=time_end)
last_upload_time = None
if last_upload:
last_upload_time = arrow.get(last_upload["created_at"])
logger.info("ProcessCGMReading: Last Nightscout bg upload: %s" % last_upload_time)
readings = []
for event in sorted(events, key=lambda x: self.timestamp_for(x)):
if last_upload_time and self.timestamp_for(event) < last_upload_time:
if self.pretend:
logger.info("ProcessCGMReading: Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
readings.append(event)
ns_entries = []
for event in readings:
ns_entries.append(self.to_nsentry(event))
return ns_entries
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload to Nightscout: %s" % entry)
else:
logger.info("Uploading to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry, entity='entries')
count += 1
return count
def timestamp_for(self, event):
# For backfills the time the event was added to the pump's event store
# might not be the time it actually occurred, so we use the egvTimestamp
return arrow.get(TANDEM_EPOCH + event.egvTimestamp)
def to_nsentry(self, event):
return NightscoutEntry.entry(
sgv = event.currentglucosedisplayvalue,
created_at = self.timestamp_for(event),
pump_event_id = event.eventId,
)
@@ -0,0 +1,83 @@
import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from ...parser.nightscout import (
CGM_START_EVENTTYPE,
NightscoutEntry
)
logger = logging.getLogger(__name__)
class ProcessCGMStartJoinStop:
def __init__(self, tconnect, nightscout, tconnect_device_id, pretend, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnect_device_id
self.pretend = pretend
self.features = features
def enabled(self):
return features.PUMP_EVENTS in self.features or features.CGM_ALERTS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessCGMStartJoinStop: querying for last uploaded entry")
last_upload = self.nightscout.last_uploaded_entry(CGM_START_EVENTTYPE, time_start=time_start, time_end=time_end)
last_upload_time = None
if last_upload:
last_upload_time = arrow.get(last_upload["created_at"])
logger.info("ProcessCGMStartJoinStop: Last Nightscout cgmstart upload: %s" % last_upload_time)
allEvents = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if self.pretend:
logger.info("ProcessCGMStartJoinStop: Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
allEvents.append(event)
allEvents.sort(lambda e: e.eventTimestamp)
ns_entries = []
for event in allEvents:
ns_entries.append(self.to_nsentry(event))
return ns_entries
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload to Nightscout: %s" % entry)
else:
logger.info("Uploading to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry)
count += 1
return count
def to_nsentry(self, event):
if type(event) in EventClass._CGM_START:
return NightscoutEntry.cgm_start(
created_at = event.eventTimestamp,
reason = "CGM Session Started",
pump_event_id = event.eventId
)
elif type(event) in EventClass._CGM_JOIN:
return NightscoutEntry.cgm_join(
created_at = event.eventTimestamp,
reason = "CGM Session Joined",
pump_event_id = event.eventId
)
elif type(event) in EventClass._CGM_STOP:
return NightscoutEntry.cgm_stop(
created_at = event.eventTimestamp,
reason = "CGM Session Stopped",
pump_event_id = event.eventId
)
@@ -0,0 +1,244 @@
import logging
import arrow
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser.utils import bitmask_to_list
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from ...parser.nightscout import (
EXERCISE_EVENTTYPE,
SLEEP_EVENTTYPE,
NightscoutEntry
)
NOT_ENDED = "Not Ended"
logger = logging.getLogger(__name__)
class ProcessUserMode:
def __init__(self, tconnect, nightscout, tconnect_device_id, pretend, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnect_device_id
self.pretend = pretend
self.features = features
def enabled(self):
return features.PUMP_EVENTS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessUserMode: querying for last uploaded exercise entry")
exercise_last_upload = self.nightscout.last_uploaded_entry(EXERCISE_EVENTTYPE, time_start=time_start, time_end=time_end)
exercise_last_upload_time = None
if exercise_last_upload:
exercise_last_upload_time = arrow.get(exercise_last_upload["created_at"])
logger.info("ProcessUserMode: Last Nightscout exercise upload: %s" % exercise_last_upload_time)
exercise_not_ended = False
if exercise_last_upload and NOT_ENDED in exercise_last_upload.get("reason", ""):
exercise_not_ended = True
logger.info("ProcessUserMode: Last exercise not ended: %s" % exercise_last_upload)
logger.debug("ProcessUserMode: querying for last uploaded sleep entry")
sleep_last_upload = self.nightscout.last_uploaded_entry(SLEEP_EVENTTYPE, time_start=time_start, time_end=time_end)
sleep_last_upload_time = None
if sleep_last_upload:
sleep_last_upload_time = arrow.get(sleep_last_upload["created_at"])
logger.info("ProcessUserMode: Last Nightscout sleep upload: %s" % sleep_last_upload_time)
sleep_not_ended = False
if sleep_last_upload and NOT_ENDED in sleep_last_upload.get("reason", ""):
sleep_not_ended = True
logger.info("ProcessUserMode: Last sleep not ended: %s" % sleep_last_upload)
last_upload_time = None
if exercise_last_upload_time and sleep_last_upload_time:
last_upload_time = max(exercise_last_upload_time, sleep_last_upload_time)
elif exercise_last_upload_time:
last_upload_time = exercise_last_upload_time
elif sleep_last_upload_time:
last_upload_time = sleep_last_upload_time
logger.info("ProcessUserMode: Last Nightscout usermode upload: %s" % last_upload_time)
ns_entries = []
processed_sleep = []
processed_exercise = []
start_sleep = None
start_exercise = None
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if self.pretend:
logger.info("ProcessUserMode: Skipping usermode event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
if self.is_start_sleep(event):
start_sleep = event
elif self.is_stop_sleep(event):
if start_sleep:
processed_sleep.append((start_sleep, event))
start_sleep = None
else:
if sleep_not_ended:
logger.info("ProcessUserMode: Found StopSleep without StartSleep, with incomplete sleep event in nightscout: %s NS: %s" % (event, sleep_last_upload))
ns_entries.append(self.process_unended_sleep_stop(event, sleep_last_upload))
else:
logger.warning("ProcessUserMode: Found StopSleep without StartSleep, and no active sleep event in nightscout: %s" % event)
elif self.is_start_exercise(event):
start_exercise = event
elif self.is_stop_exercise(event):
if start_exercise:
processed_exercise.append((start_exercise, event))
start_exercise = None
else:
if exercise_not_ended:
logger.info("ProcessUserMode: Found StopExercise without StartExercise, with incomplete exercise event in nightscout: %s NS: %s" % (event, exercise_last_upload))
ns_entries.append(self.process_unended_exercise_stop(event, exercise_last_upload))
else:
logger.warning("ProcessUserMode: Found StopExercise without StartExercise, and no active exercise event in nightscout: %s" % event)
else:
logger.warning("ProcessUserMode: not sure how to process event: %s" % event)
if start_sleep:
processed_sleep.append((start_sleep, None))
logger.info("ProcessUserMode: sleep is active")
if start_exercise:
processed_exercise.append((start_exercise, None))
logger.info("ProcessUserMode: exercise is active")
for items in processed_sleep:
ns_entries.append(self.sleep_to_nsentry(start=items[0], stop=items[1], time_end=time_end))
for items in processed_exercise:
ns_entries.append(self.exercise_to_nsentry(start=items[0], stop=items[1], time_end=time_end))
return ns_entries
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload to Nightscout: %s" % entry)
else:
logger.info("Uploading to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry)
count += 1
return count
def is_start_sleep(self, event):
return event.requestedaction == eventtypes.LidAaUserModeChange.RequestedactionEnum.StartSleep
def is_stop_sleep(self, event):
return event.requestedaction == eventtypes.LidAaUserModeChange.RequestedactionEnum.StopSleep or \
event.requestedaction == eventtypes.LidAaUserModeChange.RequestedactionEnum.StopAll
def is_start_exercise(self, event):
return event.requestedaction == eventtypes.LidAaUserModeChange.RequestedactionEnum.StartExercise
def is_stop_exercise(self, event):
return event.requestedaction == eventtypes.LidAaUserModeChange.RequestedactionEnum.StopExercise or \
event.requestedaction == eventtypes.LidAaUserModeChange.RequestedactionEnum.StopAll
def sleep_to_nsentry(self, start, stop=None, time_end=None):
if start and stop:
reason = None
if start.sleepstartedbygui == eventtypes.LidAaUserModeChange.SleepstartedbyguiEnum.TrueVal:
reason = "Sleep (Manual)"
elif start.activesleepschedule:
reason = "Sleep (Scheduled)"
duration_mins = (stop.eventTimestamp - start.eventTimestamp).seconds / 60
return NightscoutEntry.activity(
created_at=start.eventTimestamp,
reason=reason,
duration=duration_mins,
event_type=SLEEP_EVENTTYPE,
pump_event_id = "%s,%s" % (start.eventId, stop.eventId)
)
elif start:
reason = None
if start.sleepstartedbygui == eventtypes.LidAaUserModeChange.SleepstartedbyguiEnum.TrueVal:
reason = "Sleep (Manual)"
elif start.activesleepscheduleRaw:
reason = "Sleep (Scheduled)"
duration_mins = (time_end - start.eventTimestamp).seconds / 60
return NightscoutEntry.activity(
created_at=start.eventTimestamp,
reason=reason + " - " + NOT_ENDED if reason else NOT_ENDED,
duration=duration_mins,
event_type=SLEEP_EVENTTYPE,
pump_event_id = "%s" % start.eventId
)
def exercise_to_nsentry(self, start, stop=None, time_end=None):
if start and stop:
reason = "Exercise"
if start.exercisechoice == eventtypes.LidAaUserModeChange.ExercisechoiceEnum.Timed:
reason = "Exercise (Timed)"
if stop.exercisestoppedbytimer == eventtypes.LidAaUserModeChange.ExercisestoppedbytimerEnum.TrueVal:
reason += " (Stopped by timer)"
duration_mins = (stop.eventTimestamp - start.eventTimestamp).seconds / 60
return NightscoutEntry.activity(
created_at=start.eventTimestamp,
reason=reason,
duration=duration_mins,
event_type=EXERCISE_EVENTTYPE,
pump_event_id = "%s,%s" % (start.eventId, stop.eventId)
)
elif start:
reason = "Exercise"
if start.exercisechoice == eventtypes.LidAaUserModeChange.ExercisechoiceEnum.Timed:
reason = "Exercise (Timed)"
duration_mins = (time_end.eventTimestamp - start.eventTimestamp).seconds / 60
return NightscoutEntry.activity(
created_at=start.eventTimestamp,
reason=reason + " - " + NOT_ENDED,
duration=duration_mins,
event_type=EXERCISE_EVENTTYPE,
pump_event_id = "%s,%s" % (start.eventId, stop.eventId)
)
def process_unended_sleep_stop(self, event, sleep_last_upload):
logger.info("ProcessUserMode: Deleting old sleep event treatment before pushing update (delete treatments/%s)" % sleep_last_upload["_id"])
if self.pretend:
logger.info("ProcessUserMode: Skipping delete in pretend mode")
else:
self.nightscout.delete_entry('treatments/%s' % sleep_last_upload["_id"])
duration_mins = (event.eventTimestamp - arrow.get(sleep_last_upload["created_at"])).seconds / 60
return NightscoutEntry.activity(
created_at=sleep_last_upload["created_at"],
reason=sleep_last_upload["reason"].replace(" - %s" % NOT_ENDED, ""),
duration=duration_mins,
event_type=SLEEP_EVENTTYPE,
pump_event_id="%s,%s" % (sleep_last_upload.get("pump_event_id",""), event.eventId)
)
def process_unended_exercise_stop(self, event, exercise_last_upload):
logger.info("ProcessUserMode: Deleting old exercise event treatment before pushing update (delete treatments/%s)" % exercise_last_upload["_id"])
if self.pretend:
logger.info("ProcessUserMode: Skipping delete in pretend mode")
else:
self.nightscout.delete_entry('treatments/%s' % exercise_last_upload["_id"])
reason = exercise_last_upload["reason"].replace(" - %s" % NOT_ENDED, "")
if event.exercisestoppedbytimer == eventtypes.LidAaUserModeChange.ExercisestoppedbytimerEnum.TrueVal:
reason += " (Stopped by timer)"
duration_mins = (event.eventTimestamp - arrow.get(exercise_last_upload["created_at"])).seconds / 60
return NightscoutEntry.activity(
created_at=exercise_last_upload["created_at"],
reason=reason,
duration=duration_mins,
event_type=EXERCISE_EVENTTYPE,
pump_event_id="%s,%s" % (exercise_last_upload.get("pump_event_id",""), event.eventId)
)