From 362888cb148fbeff1b4ee3cf8f1fc15e37aa701c Mon Sep 17 00:00:00 2001 From: James Woglom Date: Sun, 29 Sep 2024 23:06:42 -0400 Subject: [PATCH] update profiles --- Pipfile | 1 + Pipfile.lock | 48 +- .../domain/tandemsource/pump_settings.py | 52 ++ tconnectsync/eventparser/build_events.py | 3 + tconnectsync/eventparser/events.py | 459 +++++++++++++++--- tconnectsync/eventparser/static_dicts.py | 3 +- tconnectsync/eventparser/transforms.py | 16 +- tconnectsync/parser/nightscout.py | 61 ++- tconnectsync/sync/tandemsource/process.py | 14 + .../sync/tandemsource/process_bolus.py | 14 +- .../sync/tandemsource/process_cgm_reading.py | 6 +- .../sync/tandemsource/update_profiles.py | 210 ++++++++ 12 files changed, 795 insertions(+), 92 deletions(-) create mode 100644 tconnectsync/domain/tandemsource/pump_settings.py create mode 100644 tconnectsync/sync/tandemsource/update_profiles.py diff --git a/Pipfile b/Pipfile index a142ef3..2ba6804 100644 --- a/Pipfile +++ b/Pipfile @@ -19,6 +19,7 @@ requests = {extras = ["socks"], version = "*"} requests-oidc = "*" pyjwt = "*" cryptography = "*" +dataclasses-json = "*" [scripts] tconnectsync = "python3 main.py" diff --git a/Pipfile.lock b/Pipfile.lock index 3a00b67..5eb2d58 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6f4a6601549eabda985f699484cd6f9328e06f95b05a4294b3d675599cf2fe70" + "sha256": "586b477d4c73da1d1034f852a536bf3c20a1a358406bfb5dfc1b1164f606441b" }, "pipfile-spec": 6, "requires": {}, @@ -258,6 +258,15 @@ "markers": "python_version >= '3.7'", "version": "==43.0.1" }, + "dataclasses-json": { + "hashes": [ + "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", + "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.6.7" + }, "idna": { "hashes": [ "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", @@ -411,6 +420,22 @@ "markers": "python_version >= '3.6'", "version": "==5.3.0" }, + "marshmallow": { + "hashes": [ + "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e", + "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9" + ], + "markers": "python_version >= '3.8'", + "version": "==3.22.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, "oauthlib": { "hashes": [ "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", @@ -419,6 +444,14 @@ "markers": "python_version >= '3.6'", "version": "==3.2.2" }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, "platformdirs": { "hashes": [ "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", @@ -567,6 +600,13 @@ "markers": "python_version >= '3.8'", "version": "==4.12.2" }, + "typing-inspect": { + "hashes": [ + "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", + "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78" + ], + "version": "==0.9.0" + }, "urllib3": { "hashes": [ "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", @@ -603,11 +643,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", - "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360" + "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", + "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.47" + "version": "==3.0.48" }, "ptpython": { "hashes": [ diff --git a/tconnectsync/domain/tandemsource/pump_settings.py b/tconnectsync/domain/tandemsource/pump_settings.py new file mode 100644 index 0000000..ac08f9d --- /dev/null +++ b/tconnectsync/domain/tandemsource/pump_settings.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +from dataclasses_json import dataclass_json +from typing import List + +@dataclass_json +@dataclass +class PumpProfileSegment: + startTime: int # minutes + basalRate: int # milliunits + isf: int + carbRatio: int + targetBg: int + + @property + def skip(self): + return self.startTime == 0 and self.basalRate == 0 and self.isf == 0 and self.carbRatio == 0 and self.targetBg == 0 + +@dataclass_json +@dataclass +class PumpProfile: + name: str + idp: int + tDependentSegs: List[PumpProfileSegment] + insulinDuration: int # minutes + carbEntry: int # 1 / 0 + maxBolus: int # milliunits + +@dataclass_json +@dataclass +class PumpProfiles: + activeIdp: int + profile: List[PumpProfile] + +@dataclass_json +@dataclass +class PumpGlucoseAlertSettings: + mgPerDl: int + enabled: int # 1 / 0 + duration: int # minutes + status: int # unknown + +@dataclass_json +@dataclass +class PumpCgmSettings: + highGlucoseAlert: PumpGlucoseAlertSettings + lowGlucoseAlert: PumpGlucoseAlertSettings + +@dataclass_json +@dataclass +class PumpSettings: + profiles: PumpProfiles + cgmSettings: PumpCgmSettings \ No newline at end of file diff --git a/tconnectsync/eventparser/build_events.py b/tconnectsync/eventparser/build_events.py index 9f120e9..88fe634 100644 --- a/tconnectsync/eventparser/build_events.py +++ b/tconnectsync/eventparser/build_events.py @@ -1,9 +1,12 @@ header = '''# THIS FILE IS AUTOGENERATED. DO NOT EDIT. import struct +import logging from dataclasses import dataclass from enum import Enum, IntFlag from .raw_event import RawEvent +logger = logging.getLogger(__name__) + EVENT_LEN = 26 @dataclass diff --git a/tconnectsync/eventparser/events.py b/tconnectsync/eventparser/events.py index 0e38d98..5475fed 100644 --- a/tconnectsync/eventparser/events.py +++ b/tconnectsync/eventparser/events.py @@ -1,9 +1,12 @@ # THIS FILE IS AUTOGENERATED. DO NOT EDIT. import struct +import logging from dataclasses import dataclass from enum import Enum, IntFlag from .raw_event import RawEvent +logger = logging.getLogger(__name__) + EVENT_LEN = 26 @dataclass @@ -55,7 +58,11 @@ class LidBasalRateChange(BaseEvent): @property def changetype(self): - return self.ChangetypeBitmask(self.changetypeRaw) + try: return self.ChangetypeBitmask(self.changetypeRaw) + except ValueError as e: + logger.error("Invalid changetypeRaw in ChangetypeBitmask for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -228,7 +235,11 @@ class LidAlertActivated(BaseEvent): @property def alertid(self): - return self.AlertidEnum(self.alertidRaw) + try: return self.AlertidEnum(self.alertidRaw) + except ValueError as e: + logger.error("Invalid alertidRaw in Alertid for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -401,7 +412,11 @@ class LidAlarmActivated(BaseEvent): @property def alarmid(self): - return self.AlarmidEnum(self.alarmidRaw) + try: return self.AlarmidEnum(self.alarmidRaw) + except ValueError as e: + logger.error("Invalid alarmidRaw in Alarmid for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -491,7 +506,11 @@ class LidPumpingSuspended(BaseEvent): @property def suspendreason(self): - return self.SuspendreasonEnum(self.suspendreasonRaw) + try: return self.SuspendreasonEnum(self.suspendreasonRaw) + except ValueError as e: + logger.error("Invalid suspendreasonRaw in Suspendreason for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -643,7 +662,11 @@ class LidBgReadingTaken(BaseEvent): @property def selectediob(self): - return self.SelectediobEnum(self.selectediobRaw) + try: return self.SelectediobEnum(self.selectediobRaw) + except ValueError as e: + logger.error("Invalid selectediobRaw in Selectediob for "+str(self)) + logger.error(e) + return None BgentrytypeMap = { "0": "Manual Entry by the User via Numpad", @@ -656,7 +679,11 @@ class LidBgReadingTaken(BaseEvent): @property def bgentrytype(self): - return self.BgentrytypeEnum(self.bgentrytypeRaw) + try: return self.BgentrytypeEnum(self.bgentrytypeRaw) + except ValueError as e: + logger.error("Invalid bgentrytypeRaw in Bgentrytype for "+str(self)) + logger.error(e) + return None BgsourcetypeMap = { "0": "Local Pump entry", @@ -669,7 +696,11 @@ class LidBgReadingTaken(BaseEvent): @property def bgsourcetype(self): - return self.BgsourcetypeEnum(self.bgsourcetypeRaw) + try: return self.BgsourcetypeEnum(self.bgsourcetypeRaw) + except ValueError as e: + logger.error("Invalid bgsourcetypeRaw in Bgsourcetype for "+str(self)) + logger.error(e) + return None CgmcalibrationMap = { "0": "No, it was not used to Calibrate.", @@ -682,7 +713,11 @@ class LidBgReadingTaken(BaseEvent): @property def cgmcalibration(self): - return self.CgmcalibrationEnum(self.cgmcalibrationRaw) + try: return self.CgmcalibrationEnum(self.cgmcalibrationRaw) + except ValueError as e: + logger.error("Invalid cgmcalibrationRaw in Cgmcalibration for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -751,7 +786,11 @@ class LidBolusCompleted(BaseEvent): @property def completionstatus(self): - return self.CompletionstatusEnum(self.completionstatusRaw) + try: return self.CompletionstatusEnum(self.completionstatusRaw) + except ValueError as e: + logger.error("Invalid completionstatusRaw in Completionstatus for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -814,7 +853,11 @@ class LidBolexCompleted(BaseEvent): @property def completionstatus(self): - return self.CompletionstatusEnum(self.completionstatusRaw) + try: return self.CompletionstatusEnum(self.completionstatusRaw) + except ValueError as e: + logger.error("Invalid completionstatusRaw in Completionstatus for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -985,7 +1028,11 @@ class LidAlertCleared(BaseEvent): @property def alertid(self): - return self.AlertidEnum(self.alertidRaw) + try: return self.AlertidEnum(self.alertidRaw) + except ValueError as e: + logger.error("Invalid alertidRaw in Alertid for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1151,7 +1198,11 @@ class LidAlarmCleared(BaseEvent): @property def alarmid(self): - return self.AlarmidEnum(self.alarmidRaw) + try: return self.AlarmidEnum(self.alarmidRaw) + except ValueError as e: + logger.error("Invalid alarmidRaw in Alarmid for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1268,7 +1319,11 @@ class LidBolusActivated(BaseEvent): @property def selectediob(self): - return self.SelectediobEnum(self.selectediobRaw) + try: return self.SelectediobEnum(self.selectediobRaw) + except ValueError as e: + logger.error("Invalid selectediobRaw in Selectediob for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1317,7 +1372,11 @@ class LidBolexActivated(BaseEvent): @property def selectediob(self): - return self.SelectediobEnum(self.selectediobRaw) + try: return self.SelectediobEnum(self.selectediobRaw) + except ValueError as e: + logger.error("Invalid selectediobRaw in Selectediob for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1399,7 +1458,11 @@ class LidCannulaFilled(BaseEvent): @property def completionstatus(self): - return self.CompletionstatusEnum(self.completionstatusRaw) + try: return self.CompletionstatusEnum(self.completionstatusRaw) + except ValueError as e: + logger.error("Invalid completionstatusRaw in Completionstatus for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1447,7 +1510,11 @@ class LidTubingFilled(BaseEvent): @property def completionstatus(self): - return self.CompletionstatusEnum(self.completionstatusRaw) + try: return self.CompletionstatusEnum(self.completionstatusRaw) + except ValueError as e: + logger.error("Invalid completionstatusRaw in Completionstatus for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1501,7 +1568,11 @@ class LidBolusRequestedMsg1(BaseEvent): @property def bolustype(self): - return self.BolustypeEnum(self.bolustypeRaw) + try: return self.BolustypeEnum(self.bolustypeRaw) + except ValueError as e: + logger.error("Invalid bolustypeRaw in Bolustype for "+str(self)) + logger.error(e) + return None CorrectionbolusincludedMap = { "0": "No", @@ -1514,7 +1585,11 @@ class LidBolusRequestedMsg1(BaseEvent): @property def correctionbolusincluded(self): - return self.CorrectionbolusincludedEnum(self.correctionbolusincludedRaw) + try: return self.CorrectionbolusincludedEnum(self.correctionbolusincludedRaw) + except ValueError as e: + logger.error("Invalid correctionbolusincludedRaw in Correctionbolusincluded for "+str(self)) + logger.error(e) + return None @property def carbratio(self): @@ -1578,7 +1653,11 @@ class LidBolusRequestedMsg2(BaseEvent): @property def selectediob(self): - return self.SelectediobEnum(self.selectediobRaw) + try: return self.SelectediobEnum(self.selectediobRaw) + except ValueError as e: + logger.error("Invalid selectediobRaw in Selectediob for "+str(self)) + logger.error(e) + return None OptionsMap = { "0": "Standard Bolus", @@ -1603,7 +1682,11 @@ class LidBolusRequestedMsg2(BaseEvent): @property def options(self): - return self.OptionsEnum(self.optionsRaw) + try: return self.OptionsEnum(self.optionsRaw) + except ValueError as e: + logger.error("Invalid optionsRaw in Options for "+str(self)) + logger.error(e) + return None UseroverrideMap = { "0": "\"No\", user did not override the bolus size", @@ -1616,7 +1699,11 @@ class LidBolusRequestedMsg2(BaseEvent): @property def useroverride(self): - return self.UseroverrideEnum(self.useroverrideRaw) + try: return self.UseroverrideEnum(self.useroverrideRaw) + except ValueError as e: + logger.error("Invalid useroverrideRaw in Useroverride for "+str(self)) + logger.error(e) + return None DeclinedcorrectionMap = { "0": "\"No\", user did not decline the recommended correction", @@ -1629,7 +1716,11 @@ class LidBolusRequestedMsg2(BaseEvent): @property def declinedcorrection(self): - return self.DeclinedcorrectionEnum(self.declinedcorrectionRaw) + try: return self.DeclinedcorrectionEnum(self.declinedcorrectionRaw) + except ValueError as e: + logger.error("Invalid declinedcorrectionRaw in Declinedcorrection for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1810,7 +1901,11 @@ class LidPlgsPeriodic(BaseEvent): @property def fmrstatus(self): - return self.FmrstatusEnum(self.fmrstatusRaw) + try: return self.FmrstatusEnum(self.fmrstatusRaw) + except ValueError as e: + logger.error("Invalid fmrstatusRaw in Fmrstatus for "+str(self)) + logger.error(e) + return None PgvvalidMap = { "0": "FALSE", @@ -1823,7 +1918,11 @@ class LidPlgsPeriodic(BaseEvent): @property def pgvvalid(self): - return self.PgvvalidEnum(self.pgvvalidRaw) + try: return self.PgvvalidEnum(self.pgvvalidRaw) + except ValueError as e: + logger.error("Invalid pgvvalidRaw in Pgvvalid for "+str(self)) + logger.error(e) + return None RulestateMap = { "0": "HO_SUSPEND_RULE", @@ -1838,7 +1937,11 @@ class LidPlgsPeriodic(BaseEvent): @property def rulestate(self): - return self.RulestateBitmask(self.rulestateRaw) + try: return self.RulestateBitmask(self.rulestateRaw) + except ValueError as e: + logger.error("Invalid rulestateRaw in RulestateBitmask for "+str(self)) + logger.error(e) + return None HominstateMap = { "0": "On and available", @@ -1855,7 +1958,11 @@ class LidPlgsPeriodic(BaseEvent): @property def hominstate(self): - return self.HominstateEnum(self.hominstateRaw) + try: return self.HominstateEnum(self.hominstateRaw) + except ValueError as e: + logger.error("Invalid hominstateRaw in Hominstate for "+str(self)) + logger.error(e) + return None StatusMap = { "0": "Suspend Predicted", @@ -1889,7 +1996,11 @@ class LidPlgsPeriodic(BaseEvent): @property def status(self): - return self.StatusBitmask(self.statusRaw) + try: return self.StatusBitmask(self.statusRaw) + except ValueError as e: + logger.error("Invalid statusRaw in StatusBitmask for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -1956,7 +2067,11 @@ class LidCgmAlertActivated(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2012,7 +2127,11 @@ class LidCgmAlertCleared(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2095,7 +2214,11 @@ class LidUpdateStatus(BaseEvent): @property def updatesuccessful(self): - return self.UpdatesuccessfulEnum(self.updatesuccessfulRaw) + try: return self.UpdatesuccessfulEnum(self.updatesuccessfulRaw) + except ValueError as e: + logger.error("Invalid updatesuccessfulRaw in Updatesuccessful for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2206,7 +2329,11 @@ class LidCgmJoinSessionGx(BaseEvent): @property def sessionjoinreason(self): - return self.SessionjoinreasonEnum(self.sessionjoinreasonRaw) + try: return self.SessionjoinreasonEnum(self.sessionjoinreasonRaw) + except ValueError as e: + logger.error("Invalid sessionjoinreasonRaw in Sessionjoinreason for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2278,7 +2405,11 @@ class LidCgmStopSessionGx(BaseEvent): @property def sessionstopreason(self): - return self.SessionstopreasonEnum(self.sessionstopreasonRaw) + try: return self.SessionstopreasonEnum(self.sessionstopreasonRaw) + except ValueError as e: + logger.error("Invalid sessionstopreasonRaw in Sessionstopreason for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2334,7 +2465,11 @@ class LidAaUserModeChange(BaseEvent): @property def exercisechoice(self): - return self.ExercisechoiceEnum(self.exercisechoiceRaw) + try: return self.ExercisechoiceEnum(self.exercisechoiceRaw) + except ValueError as e: + logger.error("Invalid exercisechoiceRaw in Exercisechoice for "+str(self)) + logger.error(e) + return None CurrentusermodeMap = { "0": "Normal", @@ -2351,7 +2486,11 @@ class LidAaUserModeChange(BaseEvent): @property def currentusermode(self): - return self.CurrentusermodeEnum(self.currentusermodeRaw) + try: return self.CurrentusermodeEnum(self.currentusermodeRaw) + except ValueError as e: + logger.error("Invalid currentusermodeRaw in Currentusermode for "+str(self)) + logger.error(e) + return None PrevioususermodeMap = { "0": "Normal", @@ -2368,7 +2507,11 @@ class LidAaUserModeChange(BaseEvent): @property def previoususermode(self): - return self.PrevioususermodeEnum(self.previoususermodeRaw) + try: return self.PrevioususermodeEnum(self.previoususermodeRaw) + except ValueError as e: + logger.error("Invalid previoususermodeRaw in Previoususermode for "+str(self)) + logger.error(e) + return None RequestedactionMap = { "0": "No User Request", @@ -2393,7 +2536,11 @@ class LidAaUserModeChange(BaseEvent): @property def requestedaction(self): - return self.RequestedactionEnum(self.requestedactionRaw) + try: return self.RequestedactionEnum(self.requestedactionRaw) + except ValueError as e: + logger.error("Invalid requestedactionRaw in Requestedaction for "+str(self)) + logger.error(e) + return None SleepstartedbyguiMap = { "0": "FALSE", @@ -2406,7 +2553,11 @@ class LidAaUserModeChange(BaseEvent): @property def sleepstartedbygui(self): - return self.SleepstartedbyguiEnum(self.sleepstartedbyguiRaw) + try: return self.SleepstartedbyguiEnum(self.sleepstartedbyguiRaw) + except ValueError as e: + logger.error("Invalid sleepstartedbyguiRaw in Sleepstartedbygui for "+str(self)) + logger.error(e) + return None ExercisestoppedbytimerMap = { "0": "False", @@ -2419,7 +2570,11 @@ class LidAaUserModeChange(BaseEvent): @property def exercisestoppedbytimer(self): - return self.ExercisestoppedbytimerEnum(self.exercisestoppedbytimerRaw) + try: return self.ExercisestoppedbytimerEnum(self.exercisestoppedbytimerRaw) + except ValueError as e: + logger.error("Invalid exercisestoppedbytimerRaw in Exercisestoppedbytimer for "+str(self)) + logger.error(e) + return None ActivesleepscheduleMap = { "0": "Sleep Schedule 1 is Active", @@ -2436,7 +2591,11 @@ class LidAaUserModeChange(BaseEvent): @property def activesleepschedule(self): - return self.ActivesleepscheduleBitmask(self.activesleepscheduleRaw) + try: return self.ActivesleepscheduleBitmask(self.activesleepscheduleRaw) + except ValueError as e: + logger.error("Invalid activesleepscheduleRaw in ActivesleepscheduleBitmask for "+str(self)) + logger.error(e) + return None EatingsoonstoppedbytimerMap = { "0": "False", @@ -2449,7 +2608,11 @@ class LidAaUserModeChange(BaseEvent): @property def eatingsoonstoppedbytimer(self): - return self.EatingsoonstoppedbytimerEnum(self.eatingsoonstoppedbytimerRaw) + try: return self.EatingsoonstoppedbytimerEnum(self.eatingsoonstoppedbytimerRaw) + except ValueError as e: + logger.error("Invalid eatingsoonstoppedbytimerRaw in Eatingsoonstoppedbytimer for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2515,7 +2678,11 @@ class LidAaPcmChange(BaseEvent): @property def currentpcm(self): - return self.CurrentpcmEnum(self.currentpcmRaw) + try: return self.CurrentpcmEnum(self.currentpcmRaw) + except ValueError as e: + logger.error("Invalid currentpcmRaw in Currentpcm for "+str(self)) + logger.error(e) + return None PreviouspcmMap = { "0": "No Control", @@ -2532,7 +2699,11 @@ class LidAaPcmChange(BaseEvent): @property def previouspcm(self): - return self.PreviouspcmEnum(self.previouspcmRaw) + try: return self.PreviouspcmEnum(self.previouspcmRaw) + except ValueError as e: + logger.error("Invalid previouspcmRaw in Previouspcm for "+str(self)) + logger.error(e) + return None PumpsuspendedMap = { "0": "FALSE", @@ -2545,7 +2716,11 @@ class LidAaPcmChange(BaseEvent): @property def pumpsuspended(self): - return self.PumpsuspendedEnum(self.pumpsuspendedRaw) + try: return self.PumpsuspendedEnum(self.pumpsuspendedRaw) + except ValueError as e: + logger.error("Invalid pumpsuspendedRaw in Pumpsuspended for "+str(self)) + logger.error(e) + return None CalculationavailableMap = { "0": "FALSE", @@ -2558,7 +2733,11 @@ class LidAaPcmChange(BaseEvent): @property def calculationavailable(self): - return self.CalculationavailableEnum(self.calculationavailableRaw) + try: return self.CalculationavailableEnum(self.calculationavailableRaw) + except ValueError as e: + logger.error("Invalid calculationavailableRaw in Calculationavailable for "+str(self)) + logger.error(e) + return None CgmavailableMap = { "0": "FALSE", @@ -2571,7 +2750,11 @@ class LidAaPcmChange(BaseEvent): @property def cgmavailable(self): - return self.CgmavailableEnum(self.cgmavailableRaw) + try: return self.CgmavailableEnum(self.cgmavailableRaw) + except ValueError as e: + logger.error("Invalid cgmavailableRaw in Cgmavailable for "+str(self)) + logger.error(e) + return None ClosedlooppreferredMap = { "0": "FALSE", @@ -2584,7 +2767,11 @@ class LidAaPcmChange(BaseEvent): @property def closedlooppreferred(self): - return self.ClosedlooppreferredEnum(self.closedlooppreferredRaw) + try: return self.ClosedlooppreferredEnum(self.closedlooppreferredRaw) + except ValueError as e: + logger.error("Invalid closedlooppreferredRaw in Closedlooppreferred for "+str(self)) + logger.error(e) + return None SufficientclosedloopparamsMap = { "0": "FALSE", @@ -2597,7 +2784,11 @@ class LidAaPcmChange(BaseEvent): @property def sufficientclosedloopparams(self): - return self.SufficientclosedloopparamsEnum(self.sufficientclosedloopparamsRaw) + try: return self.SufficientclosedloopparamsEnum(self.sufficientclosedloopparamsRaw) + except ValueError as e: + logger.error("Invalid sufficientclosedloopparamsRaw in Sufficientclosedloopparams for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2659,7 +2850,11 @@ class LidCgmDataGxb(BaseEvent): @property def glucosevaluestatus(self): - return self.GlucosevaluestatusEnum(self.glucosevaluestatusRaw) + try: return self.GlucosevaluestatusEnum(self.glucosevaluestatusRaw) + except ValueError as e: + logger.error("Invalid glucosevaluestatusRaw in Glucosevaluestatus for "+str(self)) + logger.error(e) + return None CgmdatatypeMap = { "0": "Five Minute Reading (FMR)", @@ -2678,7 +2873,11 @@ class LidCgmDataGxb(BaseEvent): @property def cgmDataType(self): - return self.CgmdatatypeBitmask(self.cgmDataTypeRaw) + try: return self.CgmdatatypeBitmask(self.cgmDataTypeRaw) + except ValueError as e: + logger.error("Invalid cgmDataTypeRaw in CgmdatatypeBitmask for "+str(self)) + logger.error(e) + return None @property def rate(self): @@ -2709,7 +2908,11 @@ class LidCgmDataGxb(BaseEvent): @property def egvInfoBitmask(self): - return self.EgvinfobitmaskBitmask(self.egvInfoBitmaskRaw) + try: return self.EgvinfobitmaskBitmask(self.egvInfoBitmaskRaw) + except ValueError as e: + logger.error("Invalid egvInfoBitmaskRaw in EgvinfobitmaskBitmask for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2775,7 +2978,11 @@ class LidBasalDelivery(BaseEvent): @property def commandedRateSource(self): - return self.CommandedratesourceEnum(self.commandedRateSourceRaw) + try: return self.CommandedratesourceEnum(self.commandedRateSourceRaw) + except ValueError as e: + logger.error("Invalid commandedRateSourceRaw in Commandedratesource for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2832,7 +3039,11 @@ class LidBolusDelivery(BaseEvent): @property def bolusDeliveryStatus(self): - return self.BolusdeliverystatusEnum(self.bolusDeliveryStatusRaw) + try: return self.BolusdeliverystatusEnum(self.bolusDeliveryStatusRaw) + except ValueError as e: + logger.error("Invalid bolusDeliveryStatusRaw in Bolusdeliverystatus for "+str(self)) + logger.error(e) + return None BolustypeMap = { "0": "Now", @@ -2853,7 +3064,11 @@ class LidBolusDelivery(BaseEvent): @property def bolusType(self): - return self.BolustypeBitmask(self.bolusTypeRaw) + try: return self.BolustypeBitmask(self.bolusTypeRaw) + except ValueError as e: + logger.error("Invalid bolusTypeRaw in BolustypeBitmask for "+str(self)) + logger.error(e) + return None BolussourceMap = { "0": "Pump Button", @@ -2878,7 +3093,11 @@ class LidBolusDelivery(BaseEvent): @property def bolusSource(self): - return self.BolussourceEnum(self.bolusSourceRaw) + try: return self.BolussourceEnum(self.bolusSourceRaw) + except ValueError as e: + logger.error("Invalid bolusSourceRaw in Bolussource for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -2979,7 +3198,11 @@ class LidAaDailyStatus(BaseEvent): @property def pumpcontrolstate(self): - return self.PumpcontrolstateEnum(self.pumpcontrolstateRaw) + try: return self.PumpcontrolstateEnum(self.pumpcontrolstateRaw) + except ValueError as e: + logger.error("Invalid pumpcontrolstateRaw in Pumpcontrolstate for "+str(self)) + logger.error(e) + return None UsermodeMap = { "0": "Normal", @@ -2994,7 +3217,11 @@ class LidAaDailyStatus(BaseEvent): @property def usermode(self): - return self.UsermodeEnum(self.usermodeRaw) + try: return self.UsermodeEnum(self.usermodeRaw) + except ValueError as e: + logger.error("Invalid usermodeRaw in Usermode for "+str(self)) + logger.error(e) + return None SensortypeMap = { "0": "CGM_TYPE_NONE", @@ -3011,7 +3238,11 @@ class LidAaDailyStatus(BaseEvent): @property def sensortype(self): - return self.SensortypeEnum(self.sensortypeRaw) + try: return self.SensortypeEnum(self.sensortypeRaw) + except ValueError as e: + logger.error("Invalid sensortypeRaw in Sensortype for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3069,7 +3300,11 @@ class LidCgmAlertActivatedDex(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None SensortypeMap = { "0": "Invalid", @@ -3084,7 +3319,11 @@ class LidCgmAlertActivatedDex(BaseEvent): @property def sensortype(self): - return self.SensortypeEnum(self.sensortypeRaw) + try: return self.SensortypeEnum(self.sensortypeRaw) + except ValueError as e: + logger.error("Invalid sensortypeRaw in Sensortype for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3143,7 +3382,11 @@ class LidCgmAlertClearedDex(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None SensortypeMap = { "0": "Invalid", @@ -3158,7 +3401,11 @@ class LidCgmAlertClearedDex(BaseEvent): @property def sensortype(self): - return self.SensortypeEnum(self.sensortypeRaw) + try: return self.SensortypeEnum(self.sensortypeRaw) + except ValueError as e: + logger.error("Invalid sensortypeRaw in Sensortype for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3212,7 +3459,11 @@ class LidCgmAlertAckDex(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None SensortypeMap = { "0": "Invalid", @@ -3227,7 +3478,11 @@ class LidCgmAlertAckDex(BaseEvent): @property def sensortype(self): - return self.SensortypeEnum(self.sensortypeRaw) + try: return self.SensortypeEnum(self.sensortypeRaw) + except ValueError as e: + logger.error("Invalid sensortypeRaw in Sensortype for "+str(self)) + logger.error(e) + return None AcksourceMap = { "0": "Alert Acknowledged by User", @@ -3240,7 +3495,11 @@ class LidCgmAlertAckDex(BaseEvent): @property def acksource(self): - return self.AcksourceEnum(self.acksourceRaw) + try: return self.AcksourceEnum(self.acksourceRaw) + except ValueError as e: + logger.error("Invalid acksourceRaw in Acksource for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3294,7 +3553,11 @@ class LidCgmDataFsl2(BaseEvent): @property def glucosevaluestatus(self): - return self.GlucosevaluestatusEnum(self.glucosevaluestatusRaw) + try: return self.GlucosevaluestatusEnum(self.glucosevaluestatusRaw) + except ValueError as e: + logger.error("Invalid glucosevaluestatusRaw in Glucosevaluestatus for "+str(self)) + logger.error(e) + return None CgmdatatypeMap = { "0": "Five Minute Reading (FMR)", @@ -3313,7 +3576,11 @@ class LidCgmDataFsl2(BaseEvent): @property def cgmDataType(self): - return self.CgmdatatypeBitmask(self.cgmDataTypeRaw) + try: return self.CgmdatatypeBitmask(self.cgmDataTypeRaw) + except ValueError as e: + logger.error("Invalid cgmDataTypeRaw in CgmdatatypeBitmask for "+str(self)) + logger.error(e) + return None @property def rate(self): @@ -3342,7 +3609,11 @@ class LidCgmDataFsl2(BaseEvent): @property def algorithmstate(self): - return self.AlgorithmstateEnum(self.algorithmstateRaw) + try: return self.AlgorithmstateEnum(self.algorithmstateRaw) + except ValueError as e: + logger.error("Invalid algorithmstateRaw in Algorithmstate for "+str(self)) + logger.error(e) + return None EgvinfobitmaskMap = { "0": "Five Minute Reading (FMR)", @@ -3373,7 +3644,11 @@ class LidCgmDataFsl2(BaseEvent): @property def egvInfoBitmask(self): - return self.EgvinfobitmaskBitmask(self.egvInfoBitmaskRaw) + try: return self.EgvinfobitmaskBitmask(self.egvInfoBitmaskRaw) + except ValueError as e: + logger.error("Invalid egvInfoBitmaskRaw in EgvinfobitmaskBitmask for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3472,7 +3747,11 @@ class LidCgmDataG7(BaseEvent): @property def glucosevaluestatus(self): - return self.GlucosevaluestatusEnum(self.glucosevaluestatusRaw) + try: return self.GlucosevaluestatusEnum(self.glucosevaluestatusRaw) + except ValueError as e: + logger.error("Invalid glucosevaluestatusRaw in Glucosevaluestatus for "+str(self)) + logger.error(e) + return None CgmdatatypeMap = { "0": "FMR", @@ -3491,7 +3770,11 @@ class LidCgmDataG7(BaseEvent): @property def cgmDataType(self): - return self.CgmdatatypeBitmask(self.cgmDataTypeRaw) + try: return self.CgmdatatypeBitmask(self.cgmDataTypeRaw) + except ValueError as e: + logger.error("Invalid cgmDataTypeRaw in CgmdatatypeBitmask for "+str(self)) + logger.error(e) + return None @property def rate(self): @@ -3526,7 +3809,11 @@ class LidCgmDataG7(BaseEvent): @property def algorithmstate(self): - return self.AlgorithmstateEnum(self.algorithmstateRaw) + try: return self.AlgorithmstateEnum(self.algorithmstateRaw) + except ValueError as e: + logger.error("Invalid algorithmstateRaw in Algorithmstate for "+str(self)) + logger.error(e) + return None EgvinfobitmaskMap = { "0": "Five Minute Reading (FMR)", @@ -3555,7 +3842,11 @@ class LidCgmDataG7(BaseEvent): @property def egvInfoBitmask(self): - return self.EgvinfobitmaskBitmask(self.egvInfoBitmaskRaw) + try: return self.EgvinfobitmaskBitmask(self.egvInfoBitmaskRaw) + except ValueError as e: + logger.error("Invalid egvInfoBitmaskRaw in EgvinfobitmaskBitmask for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3773,7 +4064,11 @@ class LidCgmAlertActivatedFsl2(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None SensortypeMap = { "0": "Invalid", @@ -3786,7 +4081,11 @@ class LidCgmAlertActivatedFsl2(BaseEvent): @property def sensortype(self): - return self.SensortypeEnum(self.sensortypeRaw) + try: return self.SensortypeEnum(self.sensortypeRaw) + except ValueError as e: + logger.error("Invalid sensortypeRaw in Sensortype for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): @@ -3845,7 +4144,11 @@ class LidCgmAlertClearedFsl2(BaseEvent): @property def dalertid(self): - return self.DalertidEnum(self.dalertidRaw) + try: return self.DalertidEnum(self.dalertidRaw) + except ValueError as e: + logger.error("Invalid dalertidRaw in Dalertid for "+str(self)) + logger.error(e) + return None SensortypeMap = { "0": "Invalid", @@ -3858,7 +4161,11 @@ class LidCgmAlertClearedFsl2(BaseEvent): @property def sensortype(self): - return self.SensortypeEnum(self.sensortypeRaw) + try: return self.SensortypeEnum(self.sensortypeRaw) + except ValueError as e: + logger.error("Invalid sensortypeRaw in Sensortype for "+str(self)) + logger.error(e) + return None @staticmethod def build(raw): diff --git a/tconnectsync/eventparser/static_dicts.py b/tconnectsync/eventparser/static_dicts.py index c90a0c9..bff5e96 100644 --- a/tconnectsync/eventparser/static_dicts.py +++ b/tconnectsync/eventparser/static_dicts.py @@ -139,5 +139,6 @@ CGM_ALERTS_DICT = { "20": "CGM Transmitter Error", "26": "CGM Temperature", "27": "CGM Failed Connection", - "39": "CGM Transmitter Expired" + "39": "CGM Transmitter Expired", + "40": "Pump Bluetooth Error" } \ No newline at end of file diff --git a/tconnectsync/eventparser/transforms.py b/tconnectsync/eventparser/transforms.py index ba9efff..3ed41bd 100644 --- a/tconnectsync/eventparser/transforms.py +++ b/tconnectsync/eventparser/transforms.py @@ -53,7 +53,12 @@ def transform_enum(event_def, name, name_fmt, field, tx): out += [ '@property', f'def {name_fmt}(self):', - f' return self.{enumNameFormat(name_fmt)}Enum(self.{name_fmt}Raw)', + f' try:' + f' return self.{enumNameFormat(name_fmt)}Enum(self.{name_fmt}Raw)', + f' except ValueError as e:', + f' logger.error("Invalid {name_fmt}Raw in {enumNameFormat(name_fmt)} for "+str(self))', + f' logger.error(e)', + f' return None', '' ] @@ -84,8 +89,13 @@ def transform_bitmask(event_def, name, name_fmt, field, tx): out += [ '@property', f'def {name_fmt}(self):', - f' return self.{enumNameFormat(name_fmt)}Bitmask(self.{name_fmt}Raw)', - '' + f' try:' + f' return self.{enumNameFormat(name_fmt)}Bitmask(self.{name_fmt}Raw)', + f' except ValueError as e:', + f' logger.error("Invalid {name_fmt}Raw in {enumNameFormat(name_fmt)}Bitmask for "+str(self))', + f' logger.error(e)', + f' return None', + f'' ] return out diff --git a/tconnectsync/parser/nightscout.py b/tconnectsync/parser/nightscout.py index 94daa17..b2161df 100644 --- a/tconnectsync/parser/nightscout.py +++ b/tconnectsync/parser/nightscout.py @@ -1,6 +1,7 @@ import arrow from ..domain.device_settings import Profile, DeviceSettings +from ..domain.tandemsource.pump_settings import PumpProfile, PumpSettings from ..secret import TIMEZONE_NAME, NIGHTSCOUT_PROFILE_CARBS_HR_VALUE, NIGHTSCOUT_PROFILE_DELAY_VALUE ENTERED_BY = "Pump (tconnectsync)" @@ -47,7 +48,7 @@ class NightscoutEntry: FINGER = "Finger" @staticmethod - def bolus(bolus, carbs, created_at, notes="", bg="", bg_type=""): + def bolus(bolus, carbs, created_at, notes="", bg="", bg_type="", pump_event_id=""): data = { "eventType": BOLUS_EVENTTYPE, "created_at": created_at, @@ -55,6 +56,7 @@ class NightscoutEntry: "insulin": float(bolus), "notes": notes, "enteredBy": ENTERED_BY, + "pump_event_id": pump_event_id } if bg: if bg_type: @@ -241,6 +243,57 @@ class NightscoutEntry: "units": "mg/dl" } + + + # TandemSource profile to Nightscout profile store entry + @staticmethod + def tandemsource_profile_store(profile: PumpProfile, pump_settings: PumpSettings) -> dict: + return { + # insulin duration in hours; Nightscout JS bug requires all top-level fields to be strings + "dia": "%s" % (profile.insulinDuration / 60), + "carbratio": [ + { + "time": minutes_to_ns_time(segment.startTime), + "timeAsSeconds": segment.startTime * 60, + "value": segment.carbRatio / 1000 # milliunits->units + } for segment in profile.tDependentSegs + ], + + "carbs_hr": NIGHTSCOUT_PROFILE_CARBS_HR_VALUE, + "delay": NIGHTSCOUT_PROFILE_DELAY_VALUE, + "sens": [ # Correction factor / isf + { + "time": minutes_to_ns_time(segment.startTime), + "timeAsSeconds": segment.startTime * 60, + "value": segment.isf + } for segment in profile.tDependentSegs + ], + "basal": [ + { + "time": minutes_to_ns_time(segment.startTime), + "timeAsSeconds": segment.startTime * 60, + "value": segment.basalRate / 1000 # milliunits->units + } for segment in profile.tDependentSegs + ], + "target_low": [ + { + "time": "00:00", + "timeAsSeconds": 0, + "value": pump_settings.cgmSettings.lowGlucoseAlert.mgPerDl + } + ], + "target_high": [ + { + "time": "00:00", + "timeAsSeconds": 0, + "value": pump_settings.cgmSettings.highGlucoseAlert.mgPerDl + } + ], + "timezone": TIMEZONE_NAME, # tconnectsync settings timezone + "startDate": "1970-01-01T00:00:00.000Z", + "units": "mg/dl" + } + def tandem_to_ns_time(tandem_time: str) -> str: numbers, ampm = tandem_time.split(' ') hr, min = numbers.split(':') @@ -259,6 +312,12 @@ def tandem_to_ns_time_seconds(tandem_time: str) -> int: return 60 * (60 * (12 + (int(hr) % 12)) + int(min)) raise InvalidTimeException(tandem_time) +def minutes_to_ns_time(minutes_time: int) -> str: + hr = minutes_time // 60 + mn = minutes_time % 60 + + return "%02d:%02d" % (hr, mn) + class InvalidBolusTypeException(RuntimeError): pass diff --git a/tconnectsync/sync/tandemsource/process.py b/tconnectsync/sync/tandemsource/process.py index 2922723..18e7e47 100644 --- a/tconnectsync/sync/tandemsource/process.py +++ b/tconnectsync/sync/tandemsource/process.py @@ -15,6 +15,7 @@ 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 +from .update_profiles import UpdateProfiles logger = logging.getLogger(__name__) @@ -40,6 +41,10 @@ class ProcessTimeRange: EventClass.USER_MODE.name: ProcessUserMode, } + updater_classes = [ + UpdateProfiles + ] + def process(self, time_start, time_end): logger.info(f"ProcessTimeRange {time_start=} {time_end=} {self.tconnect_device_id=} {self.features=}") @@ -77,6 +82,15 @@ class ProcessTimeRange: else: logger.info("Skipping %s, is not enabled from features %s" % (clazz, self.features)) + for updater_class in self.updater_classes: + c = updater_class(self.tconnect, self.nightscout, self.tconnect_device_id, self.pretend, self.features) + if c.enabled(): + logger.info("%s is enabled from features %s" % (updater_class.__name__, self.features)) + done = c.update(self.pretend) + logger.info("%s completed with update required: %s" % (updater_class.__name__, done)) + else: + logger.info("Skipping %s, is not enabled from features %s" % (updater_class.__name__, self.features)) + return processed_count diff --git a/tconnectsync/sync/tandemsource/process_bolus.py b/tconnectsync/sync/tandemsource/process_bolus.py index 5c37222..fd4be7e 100644 --- a/tconnectsync/sync/tandemsource/process_bolus.py +++ b/tconnectsync/sync/tandemsource/process_bolus.py @@ -60,9 +60,9 @@ class ProcessBolus: ns_entries.append(self.bolus_to_nsentry( bolusCompleted, - bolusRequested1 = m.get(events.LidBolusRequestedMsg1), - bolusRequested2 = m.get(events.LidBolusRequestedMsg2), - bolusRequested3 = m.get(events.LidBolusRequestedMsg3), + bolusRequested1 = m.get(eventtypes.LidBolusRequestedMsg1), + bolusRequested2 = m.get(eventtypes.LidBolusRequestedMsg2), + bolusRequested3 = m.get(eventtypes.LidBolusRequestedMsg3), )) return ns_entries @@ -93,14 +93,18 @@ class ProcessBolus: event_ids = [] for e in [bolusCompleted, bolusRequested1, bolusRequested2, bolusRequested3]: if e: - event_ids.append(e.eventId) + event_ids.append(str(e.eventId)) + + notes = '' + if bolusRequested2 and str(bolusRequested2.optionsRaw) in eventtypes.LidBolusRequestedMsg2.OptionsMap: + notes = eventtypes.LidBolusRequestedMsg2.OptionsMap['%d' % bolusRequested2.optionsRaw] return NightscoutEntry.bolus( bolus = bolusCompleted.insulindelivered, carbs = bolusRequested1.carbamount if bolusRequested1 and bolusRequested1.carbamount>0 else None, created_at = bolusCompleted.eventTimestamp.format(), - notes = (eventtypes.LidBolusRequestedMsg2.OptionsMap[bolusRequested2.optionsRaw] if bolusRequested2 else '') + suffix, + notes = notes + suffix, bg = bolusRequested1.BG if bolusRequested1 and bolusRequested1.BG > 0 else None, pump_event_id = ",".join(event_ids) ) diff --git a/tconnectsync/sync/tandemsource/process_cgm_reading.py b/tconnectsync/sync/tandemsource/process_cgm_reading.py index 620a7c5..41283e7 100644 --- a/tconnectsync/sync/tandemsource/process_cgm_reading.py +++ b/tconnectsync/sync/tandemsource/process_cgm_reading.py @@ -30,8 +30,10 @@ class ProcessCGMReading: 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"]) + if last_upload and "dateString" in last_upload: + last_upload_time = arrow.get(last_upload["dateString"]) + elif last_upload and "date" in last_upload: + last_upload_time = arrow.get(last_upload["date"]) logger.info("ProcessCGMReading: Last Nightscout bg upload: %s" % last_upload_time) readings = [] diff --git a/tconnectsync/sync/tandemsource/update_profiles.py b/tconnectsync/sync/tandemsource/update_profiles.py new file mode 100644 index 0000000..95c3cef --- /dev/null +++ b/tconnectsync/sync/tandemsource/update_profiles.py @@ -0,0 +1,210 @@ +import logging +import arrow +import copy +import json +from typing import Tuple + +from ...features import DEFAULT_FEATURES +from ... import features +from ...domain.tandemsource.pump_settings import PumpSettings +from ...parser.nightscout import ( + NightscoutEntry +) +from ...secret import NIGHTSCOUT_PROFILE_UPLOAD_MODE + +logger = logging.getLogger(__name__) + +def _get_default_upload_mode(): + return NIGHTSCOUT_PROFILE_UPLOAD_MODE + +class UpdateProfiles: + 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.PROFILES in self.features + + def update(self, pretend): + upload_mode = _get_default_upload_mode() + logger.debug("UpdateProfiles: getting Tandem Source profile data") + + all_metadata = self.tconnect.tandemsource.pump_event_metadata() + pump_meta = None + for m in all_metadata: + if m['tconnectDeviceId'] == self.tconnect_device_id: + pump_meta = m + + if not pump_meta: + return False + + raw_settings = pump_meta.get("lastUpload", {}).get("settings") + if not raw_settings: + return False + + pump_settings = PumpSettings.from_dict(raw_settings) + logger.info("Current pump settings: %s" % pump_settings) + + ns_profile_obj = self.nightscout.current_profile() + logger.info("Current Nightscout profile: %s" % ns_profile_obj) + if ns_profile_obj is None: + ns_profile_obj = {} + + diff, ns_profile_new = self.compare_profiles(pump_settings, ns_profile_obj) + if not diff: + logger.info("Pump and Nightscout profiles up to date") + return False + + if upload_mode == 'add': + profile_to_upload = self.setup_new_profile(ns_profile_new) + logger.info("Adding new Nightscout profiles object: %s", profile_to_upload) + + if not pretend: + self.nightscout.upload_entry(profile_to_upload, entity='profile') + return True + + elif upload_mode == 'replace': + logger.info("Replacing new Nightscout profiles object: %s", ns_profile_new) + + if not pretend: + self.nightscout.put_entry(ns_profile_new, entity='profile') + return True + + else: + raise RuntimeError('invalid upload_mode: %s' % upload_mode) + + + """ + Compare pump device and Nightscout profiles, and return a final dictionary of + Nightscout profile objects, with the pump profile settings overriding what is + currently in Nightscout. + + ns_profile_obj is the output from NightscoutApi.current_profile() and should be the most + recent profile object in mongo. + + Returns the new Nightscout profile and whether it was changed. + """ + def compare_profiles(self, pump_settings: PumpSettings, ns_profile_obj: dict) -> Tuple[bool, dict]: + device = {profile.name: profile for profile in pump_settings.profiles.profile} + activeIdp = pump_settings.profiles.activeIdp + + ns = ns_profile_obj.get('store', {}) + + logger.info("compare_profiles profile names: device: %s ns: %s", device.keys(), ns.keys()) + + new_ns_profile = copy.deepcopy(ns_profile_obj) + if not 'store' in new_ns_profile: + new_ns_profile['store'] = {} + updated_ns_profile = False + + missing_profiles_in_ns = set(device.keys()) - set(ns.keys()) + for profile_name in missing_profiles_in_ns: + logger.info("Missing %s profile in Nightscout: %s", profile_name, device.get(profile_name)) + pump_configured_profile = device[profile_name] + ns_translated_profile = NightscoutEntry.tandemsource_profile_store(pump_configured_profile, pump_settings) + logger.info("Will add %s profile to Nightscout: %s", profile_name, ns_translated_profile) + new_ns_profile['store'][profile_name] = ns_translated_profile + updated_ns_profile = True + + existent_profiles_in_ns = set(device.keys()) & set(ns.keys()) + for profile_name in existent_profiles_in_ns: + logger.debug("Checking for differences for %s profile between pump and nightscout", profile_name) + pump_configured_profile = device[profile_name] + ns_translated_profile = NightscoutEntry.tandemsource_profile_store(pump_configured_profile, pump_settings) + ns_configured_profile = ns[profile_name] + + logger.debug("Comparing %s profile from pump: %s to nightscout: %s", profile_name, ns_translated_profile, ns_configured_profile) + if self.nightscout_profiles_identical(ns_configured_profile, ns_translated_profile): + logger.info("Profile %s identical between pump and nightscout", profile_name) + continue + + logger.info("Profile %s needs update in nightscout: %s", profile_name, ns_translated_profile) + new_ns_profile['store'][profile_name] = ns_translated_profile + updated_ns_profile = True + + current_pump_profile = None + for profile in pump_settings.profiles.profile: + if profile.idp == activeIdp: + current_pump_profile = profile.name + + if not current_pump_profile: + logger.error('No current pump profile, so skipping profile update') + return False, ns_profile_obj + + current_ns_profile = ns_profile_obj.get('defaultProfile') + if current_pump_profile != current_ns_profile: + logger.info("Current profile changed: pump: %s nightscout: %s", current_pump_profile, current_ns_profile) + new_ns_profile['defaultProfile'] = current_pump_profile + updated_ns_profile = True + + if not updated_ns_profile: + logger.info("No Nightscout profile changes") + return False, ns_profile_obj + + logger.info("New Nightscout profile object: %s", new_ns_profile) + return True, new_ns_profile + + def nightscout_profiles_identical(self, configured: dict, translated: dict) -> bool: + if configured == translated: + logger.debug("direct dicts equal") + return True + + if json.dumps(configured, sort_keys=True, indent=None) == json.dumps(translated, sort_keys=True, indent=None): + logger.debug("initial JSON dump identical") + return True + + # convert all JSON values into strings + def map_nested_dicts_modify(ob, func): + for k, v in ob.items(): + if isinstance(v, dict): + map_nested_dicts_modify(v, func) + elif isinstance(v, list): + map_nested_lists_modify(v, func) + else: + ob[k] = func(v) + + def map_nested_lists_modify(ob, func): + for i in range(len(ob)): + v = ob[i] + if isinstance(v, dict): + map_nested_dicts_modify(v, func) + elif isinstance(v, list): + map_nested_lists_modify(v, func) + else: + ob[i] = func(v) + + def to_numeric(x): + if type(x) in [int, float]: + return '%f' % x + try: + return '%f' % float(x) + except (ValueError, TypeError): + return x + + + convert_func = lambda x: to_numeric(x) + + configured_str = json.loads(json.dumps(configured)) + map_nested_dicts_modify(configured_str, convert_func) + translated_str = json.loads(json.dumps(translated)) + map_nested_dicts_modify(translated_str, convert_func) + + if json.dumps(configured_str, sort_keys=True, indent=None) == json.dumps(translated_str, sort_keys=True, indent=None): + logger.debug("map_nested_dicts JSON dump identical") + return True + + logger.debug("profiles not identical") + return False + + def setup_new_profile(self, ns_profile: dict) -> dict: + if '_id' in ns_profile: + del ns_profile['_id'] + + now = arrow.now().isoformat() + ns_profile['startDate'] = now + ns_profile['created_at'] = now + + return ns_profile