Compare commits

...
52 Commits
Author SHA1 Message Date
James Woglom a028cdf3fe v2.1.8 2024-12-12 00:29:19 -05:00
James Woglom 093b42ff94 fix codecov 2024-12-12 00:29:11 -05:00
James Woglom 00023c6aba fix flake8 2024-12-12 00:27:35 -05:00
James Woglom 1ed3d35965 v2.1.7 2024-12-12 00:23:45 -05:00
James Woglom 9493b6546f process_alarm exclude resumepumpalarm and test 2024-12-12 00:23:17 -05:00
James Woglom 24a635ca21 add process_device_status and process_user_mode tests 2024-12-12 00:09:28 -05:00
James Woglom 80824906d9 add unit test 2024-12-11 22:41:58 -05:00
James Woglom e5259a7981 attempt to mutate battery charge percent 2024-12-11 00:56:01 -05:00
James Woglom 650fb60988 support excluding event id filter 2024-12-11 00:50:40 -05:00
James Woglom 2b367d45f3 eventparser: use eventID consistently as seqNum 2024-12-10 20:35:17 -05:00
James Woglom dd2dcd17bd eventparser: add custom events 2024-12-10 17:48:51 -05:00
James Woglom 98b7edc904 eventparser: handle unknown ids in Event() 2024-12-10 17:44:54 -05:00
James Woglom e82d311fd6 eventparser: move baseevent 2024-12-10 17:44:32 -05:00
James Woglom 9a2a4ca771 v2.1.6 2024-10-16 21:34:22 -04:00
James Woglom 54ba2ed7df profiles: sort and set enteredBy 2024-10-16 21:34:11 -04:00
James Woglom 2892eb8ace exclude empty pump profile segments 2024-10-16 21:13:41 -04:00
James Woglom 1d432982ad v2.1.5 with pypi upload fix 2024-10-07 21:59:01 -04:00
James Woglom 891c2e2c10 fix printf 2024-10-07 21:30:50 -04:00
James Woglom 7dc479c84b v2.1.4 2024-10-05 12:56:17 -04:00
James Woglom 0417fb9fd8 add IGNORE_ZERO_UNIT_BASAL 2024-10-05 12:55:59 -04:00
James Woglom b80d52662b v2.1.3 2024-10-03 01:00:16 -04:00
Zack FernandesandJames Woglom 51ac469f00 Fix choose_device import class name 2024-10-03 00:59:45 -04:00
James Woglom 81b13f236a v2.1.2 2024-10-02 23:07:43 -04:00
James Woglom d57b3872f1 fix process_user_mode again 2024-10-02 23:07:34 -04:00
James Woglom 0609a46ed2 v2.1.1 2024-10-02 22:23:39 -04:00
James Woglom e63d725c2b process_user_mode: fix bug when exercise concludes 2024-10-02 22:23:24 -04:00
James Woglom fec964e767 v2.1.0 2024-10-02 02:14:28 -04:00
James Woglom 366182f65b fix timezone transformation logic 2024-10-02 02:14:01 -04:00
James Woglom d9d09dddb3 v2.0.9 2024-10-02 01:35:10 -04:00
James Woglom 04643255fe heroku_helpers 2024-10-02 01:34:47 -04:00
James Woglom c2f7070f3b no 3.7 build job 2024-10-02 00:56:38 -04:00
James Woglom 9c35f850b9 v2.0.8 2024-10-02 00:54:29 -04:00
James Woglom a7383b0796 fix return code 2024-10-02 00:52:41 -04:00
James Woglom 4f52927d2f try to fix jwt import error 2024-10-02 00:45:39 -04:00
James Woglom 5b938c010d only conditionally import typing_extensions 2024-10-02 00:39:56 -04:00
James Woglom 3e50c7018d py3.7 force pyopenssl 2024-10-01 21:02:04 -04:00
James Woglom 32f3639eb0 py3.7 actions 2024-10-01 17:29:49 -04:00
James Woglom 9a7cb1bdfd no anchors 2024-10-01 17:27:01 -04:00
James Woglom ead7ab883b check old python versions 2024-10-01 17:25:26 -04:00
James Woglom ff395ffa3e test on py3.7 2024-10-01 17:17:55 -04:00
James Woglom 8f2ad586fd v2.0.7: fix py3.7 compat 2024-10-01 17:17:33 -04:00
James Woglom 828cccaf10 v2.0.6: update install_requires packages 2024-10-01 17:05:54 -04:00
James Woglom ae26102df2 v2.0.5 2024-10-01 17:01:40 -04:00
James Woglom 152c263fe0 Use pyjwt v2.8 to allow python3.7 support 2024-10-01 16:56:19 -04:00
James Woglom 38c933a257 v2.0.4 2024-10-01 16:50:19 -04:00
James Woglom d507e47e7a convert to float 2024-10-01 16:44:20 -04:00
James Woglom 8c2f41c2d5 skip processing no basal events 2024-10-01 16:30:18 -04:00
James Woglom 018c6d2228 dont repeat the last event on a process cycle 2024-10-01 16:29:23 -04:00
James Woglom 32ba49dc8a autoupdate bugfix 2024-10-01 16:24:43 -04:00
James Woglom a5cbc5d612 v2.0.3 2024-10-01 16:08:06 -04:00
James Woglom ef515021cc check changes for last seen event id 2024-10-01 16:07:48 -04:00
James Woglom 4af54bf7cb set tzinfo (fix #100) 2024-10-01 15:59:20 -04:00
41 changed files with 1450 additions and 234 deletions
+2 -2
View File
@@ -5,9 +5,9 @@ coverage:
project:
default: false
tconnectsync:
paths:
paths:
- "tconnectsync/"
target: '75%'
target: '60%'
threshold: '5%'
tests:
paths:
+1 -1
View File
@@ -28,6 +28,6 @@ jobs:
--outdir dist/
.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
+56 -53
View File
@@ -18,57 +18,60 @@ jobs:
python-version: ['3.8', '3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest pipenv
pipenv install --system
- name: Run pipenv check
run: |
# DDoS attacks in wheel and setuptools packages, not relevant
# root certificate store, not relevant
pipenv check \
--ignore 51499 \
--ignore 52495 \
--ignore 52365 \
--ignore 59956 \
--ignore 58755 \
--ignore 67895 \
--ignore 61893 \
--ignore 61601 \
--ignore 62044 \
--ignore 67599 \
--ignore 72083 \
--ignore 71064 \
--ignore 71608 \
--ignore 72236
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
- name: Check codecov configuration
run: |
curl -X POST --data-binary @.codecov.yml https://codecov.io/validate
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest pipenv
pipenv install --system
- name: Run pipenv check
run: |
# DDoS attacks in wheel and setuptools packages, not relevant
# root certificate store, not relevant
pipenv check \
--ignore 51499 \
--ignore 52495 \
--ignore 52365 \
--ignore 59956 \
--ignore 58755 \
--ignore 67895 \
--ignore 61893 \
--ignore 61601 \
--ignore 62044 \
--ignore 67599 \
--ignore 72083 \
--ignore 71064 \
--ignore 71608 \
--ignore 72236
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Run tconnectsync --help
run: |
tconnectsync --help
- name: Test with pytest
run: |
pytest
- name: Check codecov configuration
run: |
curl -X POST --data-binary @.codecov.yml https://codecov.io/validate
if [[ "$(curl -s -o /dev/null -w "%{http_code}" -X POST --data-binary @.codecov.yml https://codecov.io/validate)" != "200" ]]; then
echo Error parsing codecov file
exit 1
fi
- name: Generate Coverage Report
run: |
pip install coverage
coverage run -m unittest
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: false
if [[ "$(curl -s -o /dev/null -w "%{http_code}" -X POST --data-binary @.codecov.yml https://codecov.io/validate)" != "200" ]]; then
echo Error parsing codecov file
exit 1
fi
- name: Generate Coverage Report
run: |
pip install coverage
coverage run -m unittest
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: false
+3 -1
View File
@@ -17,12 +17,14 @@ pysocks = "*"
urllib3 = "==1.26.6"
requests = {extras = ["socks"], version = "==2.31.0"}
requests-oidc = "*"
pyjwt = "*"
PyJWT = "==2.8.0"
cryptography = "*"
dataclasses-json = "*"
cffi = ">=1.15.1"
typing-extensions = "*"
[scripts]
tconnectsync = "python3 main.py"
test = "python3 -m unittest discover -vv"
build_events = "bash -c 'cd tconnectsync/eventparser && python3 build_events.py > events.py'"
lint = "bash -c 'flake8 . --count --select=E9,F63,F7,F82 && flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 && echo PASS'"
Generated
+6 -5
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "74ba9aa4c7c93f3533b986f868b63bea54995dbd8d37bde4745bc9023de66dc7"
"sha256": "1d28c5104c37e390b88704d1453eec15e5c6df241ca85f82a88973f8aee2d1d1"
},
"pipfile-spec": 6,
"requires": {},
@@ -471,12 +471,12 @@
},
"pyjwt": {
"hashes": [
"sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850",
"sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"
"sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
"sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==2.9.0"
"markers": "python_version >= '3.7'",
"version": "==2.8.0"
},
"pypng": {
"hashes": [
@@ -590,6 +590,7 @@
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.12.2"
},
-6
View File
@@ -1,6 +0,0 @@
coverage:
status:
project:
default:
target: 50%
threshold: null
+12 -2
View File
@@ -1,9 +1,9 @@
[metadata]
name = tconnectsync
version = 2.0.2
version = 2.1.8
author = James Woglom
author_email = j@wogloms.net
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
description = Syncs Tandem Source (formerly t:connect) insulin pump data to Nightscout for the t:slim X2
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/jwoglom/tconnectsync
@@ -25,6 +25,16 @@ install_requires =
arrow
lxml
python-dotenv
requests-mock
pysocks
urllib3
requests
requests-oidc
PyJWT
cryptography
dataclasses-json
cffi
typing-extensions
[options.packages.find]
where = .
+10 -2
View File
@@ -4,6 +4,12 @@ import arrow
import argparse
import logging
import pkg_resources
import typing
# Required for cryptography lib in python 3.7
if sys.version_info < (3, 8):
import typing_extensions
typing.Protocol = typing_extensions.Protocol
from .api import TConnectApi
from .process import process_time_range
@@ -114,6 +120,8 @@ def main(*args, **kwargs):
sys.exit(u.process(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features))
else:
tconnectDevice = TandemSourceChooseDevice(secret, tconnect).choose()
added = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, features=args.features).process(time_start, time_end)
sys.exit(added)
added, last_event_id = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, features=args.features).process(time_start, time_end)
# return exit code 0 if processed events
sys.exit(0 if added>0 else 1)
+27 -6
View File
@@ -19,6 +19,7 @@ from jwt.algorithms import RSAAlgorithm
from ..util import timeago, cap_length
from .common import parse_ymd_date, base_headers, base_session, ApiException, ApiLoginException
from ..secret import CACHE_CREDENTIALS, CACHE_CREDENTIALS_PATH
from ..eventparser.generic import Events, decode_raw_events, EVENT_LEN
logger = logging.getLogger(__name__)
@@ -357,22 +358,42 @@ class TandemSourceApi:
def pump_event_metadata(self):
return self.get('api/reports/reportsfacade/%s/pumpeventmetadata' % (self.pumperId), {})
DEFAULT_EVENT_IDS = [229,5,28,4,26,99,279,3,16,59,21,55,20,280,64,65,66,61,33,371,171,369,460,172,370,461,372,399,256,213,406,394,212,404,214,405,447,313,60,14,6,90,230,140,12,11,53,13,63,203,307,191]
"""
Returns raw unparsed string for pump events
tconnect_device_id is "tconnectDeviceId" from pump_event_metadata()
"""
def pump_events_raw(self, tconnect_device_id, min_date=None, max_date=None):
def pump_events_raw(self, tconnect_device_id, min_date=None, max_date=None, event_ids_filter=DEFAULT_EVENT_IDS):
minDate = parse_ymd_date(min_date)
maxDate = parse_ymd_date(max_date)
logger.debug(f'pump_events_raw({minDate}, {maxDate})')
logger.debug(f'pump_events_raw({tconnect_device_id}, {minDate}, {maxDate})')
# 229,5,28,4,26,99,279,3,16,59,21,55,20,280,64,65,66,61,33,371,171,369,460,172,370,461,372,399,256,213,406,394,212,404,214,405,447,313,60,14,6,90,230,140,12,11,53,13,63,203,307,191
eventIdsFilter = '229%2C5%2C28%2C4%2C26%2C99%2C279%2C3%2C16%2C59%2C21%2C55%2C20%2C280%2C64%2C65%2C66%2C61%2C33%2C371%2C171%2C369%2C460%2C172%2C370%2C461%2C372%2C399%2C256%2C213%2C406%2C394%2C212%2C404%2C214%2C405%2C447%2C313%2C60%2C14%2C6%2C90%2C230%2C140%2C12%2C11%2C53%2C13%2C63%2C203%2C307%2C191'
return self.get('api/reports/reportsfacade/pumpevents/%s/%s?minDate=%s&maxDate=%s&eventIds=%s' % (
# default: 229,5,28,4,26,99,279,3,16,59,21,55,20,280,64,65,66,61,33,371,171,369,460,172,370,461,372,399,256,213,406,394,212,404,214,405,447,313,60,14,6,90,230,140,12,11,53,13,63,203,307,191
eventIdsFilter = '%2C'.join(map(str, event_ids_filter)) if event_ids_filter else None
return self.get('api/reports/reportsfacade/pumpevents/%s/%s?minDate=%s&maxDate=%s%s' % (
self.pumperId,
tconnect_device_id,
minDate,
maxDate,
eventIdsFilter
'&eventIds=%s' % eventIdsFilter if eventIdsFilter else ''
), {})
"""
Fetch and decode pump events using eventparser.
Default of fetch_all_events=False will filter to the same eventids used in the Tandem Source backend.
If fetch_all_events=True, then all event types from the history log will be returned.
"""
def pump_events(self, tconnect_device_id, min_date=None, max_date=None, fetch_all_event_types=False):
pump_events_raw = self.pump_events_raw(
tconnect_device_id,
min_date,
max_date,
event_ids_filter=None if fetch_all_event_types else self.DEFAULT_EVENT_IDS
)
pump_events_decoded = decode_raw_events(pump_events_raw)
logger.info(f"Read {len(pump_events_decoded)} bytes (est. {len(pump_events_decoded)/EVENT_LEN} events)")
return Events(pump_events_decoded)
@@ -25,6 +25,9 @@ class PumpProfile:
carbEntry: int # 1 / 0
maxBolus: int # milliunits
def __post_init__(self):
self.tDependentSegs = [i for i in self.tDependentSegs if not i.skip]
@dataclass_json
@dataclass
class PumpProfiles:
+20 -14
View File
@@ -3,18 +3,12 @@ import struct
import logging
from dataclasses import dataclass
from enum import Enum, IntFlag
from .raw_event import RawEvent
from .raw_event import RawEvent, BaseEvent
logger = logging.getLogger(__name__)
EVENT_LEN = 26
@dataclass
class BaseEvent:
@staticmethod
def build(raw):
raise NotImplemented
'''
TYPE_TO_STRUCT = {
@@ -67,9 +61,13 @@ class {name}(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
'''
def firstLower(text):
@@ -85,13 +83,13 @@ def eventNameFormat(text):
def fieldNameFormat(text):
if not text or all([i.isupper() for i in text]):
return text
return firstLower(text.replace('_', ' ').title().replace(' ', ''))
return firstLower(text.replace('_', ' ').title().replace(' ', '')).replace('raw', 'Raw')
def build_fields(event_def):
ret = []
for name, field in event_def["data"].items():
suffix = 'Raw' if "transform" in field else ''
suffix = 'Raw' if "transform" in field and name[-3:] != 'Raw' else ''
f = f'{fieldNameFormat(name)}{suffix}: {TYPE_TO_PYOBJ[field["type"]]}'
if "uom" in field:
f += ' # ' + field['uom']
@@ -107,7 +105,7 @@ def build_decode(event_def):
p1 = f'{fieldNameFormat(name)}, = {unpack_command_for(field)}'
p1s.append(p1)
suffix = 'Raw' if "transform" in field else ''
suffix = 'Raw' if "transform" in field and name[-3:] != 'Raw' else ''
p2 = f'{fieldNameFormat(name)}{suffix} = {fieldNameFormat(name)},'
p2s.append(p2)
@@ -160,13 +158,21 @@ def build_events_map(events):
if __name__ == '__main__':
import json
merged_events = {}
output = f'{header}'
with open("events.json", "r") as f:
j = json.loads(f.read())
for event_id, event_def in j["events"].items():
output += build_event(event_id, event_def)
merged_events.update(j["events"])
with open("custom_events.json", "r") as f:
j = json.loads(f.read())
merged_events.update(j["events"])
for event_id, event_def in merged_events.items():
output += build_event(event_id, event_def)
output += build_events_map(merged_events)
output += build_events_map(j["events"])
print(output)
@@ -0,0 +1,67 @@
{
"events": {
"81": {
"name": "LID_DAILY_BASAL",
"data": {
"dailyTotalBasal": {
"type": "float32",
"offset": 0,
"uom": "units"
},
"lastBasalRate": {
"type": "float32",
"offset": 4,
"uom": "units/hour"
},
"iob": {
"type": "float32",
"offset": 8,
"uom": "units"
},
"batteryChargePercentMSBRaw": {
"type": "uint8",
"offset": 12
},
"batteryChargePercentLSBRaw": {
"type": "uint8",
"offset": 13,
"transform": [["battery_charge_percent", ""]]
},
"batteryLipoMilliVolts": {
"type": "uint16",
"offset": 14
}
}
},
"48": {
"name": "LID_CARBS_ENTERED",
"data": {
"carbs": {
"type": "float32",
"offset": 0,
"uom": "carbs"
}
}
},
"36": {
"name": "LID_USB_CONNECTED",
"data": {
"negotiatedCurrent": {
"type": "float32",
"offset": 0,
"uom": "mA"
}
}
},
"37": {
"name": "LID_USB_DISCONNECTED",
"data": {
"negotiatedCurrent": {
"type": "float32",
"offset": 0,
"uom": "mA"
}
}
}
}
}
+421 -65
View File
@@ -3,18 +3,12 @@ import struct
import logging
from dataclasses import dataclass
from enum import Enum, IntFlag
from .raw_event import RawEvent
from .raw_event import RawEvent, BaseEvent
logger = logging.getLogger(__name__)
EVENT_LEN = 26
@dataclass
class BaseEvent:
@staticmethod
def build(raw):
raise NotImplemented
UINT8 = '>B'
INT8 = '>b'
UINT16 = '>H'
@@ -87,9 +81,13 @@ class LidBasalRateChange(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAlertActivated(BaseEvent):
@@ -263,9 +261,13 @@ class LidAlertActivated(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAlarmActivated(BaseEvent):
@@ -441,9 +443,13 @@ class LidAlarmActivated(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidMalfunctionActivated(BaseEvent):
@@ -478,9 +484,13 @@ class LidMalfunctionActivated(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidPumpingSuspended(BaseEvent):
@@ -536,9 +546,13 @@ class LidPumpingSuspended(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidPumpingResumed(BaseEvent):
@@ -567,9 +581,13 @@ class LidPumpingResumed(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidTimeChanged(BaseEvent):
@@ -580,20 +598,20 @@ class LidTimeChanged(BaseEvent):
raw: RawEvent
timeprior: int # ms
timeafter: int # ms
rawrtctime: int # ms
Rawrtctime: int # ms
@staticmethod
def build(raw):
timeprior, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 10)
timeafter, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 14)
rawrtctime, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 18)
Rawrtctime, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 18)
return LidTimeChanged(
raw = RawEvent.build(raw),
timeprior = timeprior,
timeafter = timeafter,
rawrtctime = rawrtctime,
Rawrtctime = Rawrtctime,
)
@property
@@ -601,9 +619,13 @@ class LidTimeChanged(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidDateChanged(BaseEvent):
@@ -614,20 +636,20 @@ class LidDateChanged(BaseEvent):
raw: RawEvent
dateprior: int # day
dateafter: int # day
rawrtctime: int # ms
Rawrtctime: int # ms
@staticmethod
def build(raw):
dateprior, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 10)
dateafter, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 14)
rawrtctime, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 18)
Rawrtctime, = struct.unpack_from(UINT32, raw[:EVENT_LEN], 18)
return LidDateChanged(
raw = RawEvent.build(raw),
dateprior = dateprior,
dateafter = dateafter,
rawrtctime = rawrtctime,
Rawrtctime = Rawrtctime,
)
@property
@@ -635,9 +657,13 @@ class LidDateChanged(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBgReadingTaken(BaseEvent):
@@ -755,9 +781,13 @@ class LidBgReadingTaken(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolusCompleted(BaseEvent):
@@ -823,9 +853,13 @@ class LidBolusCompleted(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolexCompleted(BaseEvent):
@@ -891,9 +925,13 @@ class LidBolexCompleted(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAlertCleared(BaseEvent):
@@ -1061,9 +1099,13 @@ class LidAlertCleared(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAlarmCleared(BaseEvent):
@@ -1230,9 +1272,13 @@ class LidAlarmCleared(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCartridgeFilled(BaseEvent):
@@ -1261,9 +1307,13 @@ class LidCartridgeFilled(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidShelfMode(BaseEvent):
@@ -1304,9 +1354,13 @@ class LidShelfMode(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolusActivated(BaseEvent):
@@ -1358,9 +1412,13 @@ class LidBolusActivated(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolexActivated(BaseEvent):
@@ -1412,9 +1470,13 @@ class LidBolexActivated(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidDataLogCorruption(BaseEvent):
@@ -1443,9 +1505,13 @@ class LidDataLogCorruption(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCannulaFilled(BaseEvent):
@@ -1495,9 +1561,13 @@ class LidCannulaFilled(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidTubingFilled(BaseEvent):
@@ -1550,9 +1620,13 @@ class LidTubingFilled(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolusRequestedMsg1(BaseEvent):
@@ -1639,9 +1713,13 @@ class LidBolusRequestedMsg1(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolusRequestedMsg2(BaseEvent):
@@ -1774,9 +1852,13 @@ class LidBolusRequestedMsg2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolusRequestedMsg3(BaseEvent):
@@ -1811,9 +1893,13 @@ class LidBolusRequestedMsg3(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidNewDay(BaseEvent):
@@ -1845,9 +1931,13 @@ class LidNewDay(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidArmInit(BaseEvent):
@@ -1882,9 +1972,13 @@ class LidArmInit(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidPlgsPeriodic(BaseEvent):
@@ -2057,9 +2151,13 @@ class LidPlgsPeriodic(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertActivated(BaseEvent):
@@ -2123,9 +2221,13 @@ class LidCgmAlertActivated(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertCleared(BaseEvent):
@@ -2180,9 +2282,13 @@ class LidCgmAlertCleared(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidVersionInfo(BaseEvent):
@@ -2217,9 +2323,13 @@ class LidVersionInfo(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidUpdateStatus(BaseEvent):
@@ -2280,9 +2390,13 @@ class LidUpdateStatus(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmStartSessionGx(BaseEvent):
@@ -2314,9 +2428,13 @@ class LidCgmStartSessionGx(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmJoinSessionGx(BaseEvent):
@@ -2390,9 +2508,13 @@ class LidCgmJoinSessionGx(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmStopSessionGx(BaseEvent):
@@ -2469,9 +2591,13 @@ class LidCgmStopSessionGx(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAaUserModeChange(BaseEvent):
@@ -2688,9 +2814,13 @@ class LidAaUserModeChange(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAaPcmChange(BaseEvent):
@@ -2867,9 +2997,13 @@ class LidAaPcmChange(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmDataGxb(BaseEvent):
@@ -2998,9 +3132,13 @@ class LidCgmDataGxb(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBasalDelivery(BaseEvent):
@@ -3061,9 +3199,13 @@ class LidBasalDelivery(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidBolusDelivery(BaseEvent):
@@ -3189,9 +3331,13 @@ class LidBolusDelivery(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidVersionsA(BaseEvent):
@@ -3226,9 +3372,13 @@ class LidVersionsA(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidAaDailyStatus(BaseEvent):
@@ -3323,9 +3473,13 @@ class LidAaDailyStatus(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertActivatedDex(BaseEvent):
@@ -3412,9 +3566,13 @@ class LidCgmAlertActivatedDex(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertClearedDex(BaseEvent):
@@ -3492,9 +3650,13 @@ class LidCgmAlertClearedDex(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertAckDex(BaseEvent):
@@ -3593,9 +3755,13 @@ class LidCgmAlertAckDex(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmDataFsl2(BaseEvent):
@@ -3758,9 +3924,13 @@ class LidCgmDataFsl2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmJoinSessionG7(BaseEvent):
@@ -3789,9 +3959,13 @@ class LidCgmJoinSessionG7(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmDataG7(BaseEvent):
@@ -3960,9 +4134,13 @@ class LidCgmDataG7(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmStartSessionFsl2(BaseEvent):
@@ -3991,9 +4169,13 @@ class LidCgmStartSessionFsl2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmStopSessionFsl2(BaseEvent):
@@ -4028,9 +4210,13 @@ class LidCgmStopSessionFsl2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmJoinSessionFsl2(BaseEvent):
@@ -4065,9 +4251,13 @@ class LidCgmJoinSessionFsl2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmStopSessionG7(BaseEvent):
@@ -4108,9 +4298,13 @@ class LidCgmStopSessionG7(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertActivatedFsl2(BaseEvent):
@@ -4195,9 +4389,13 @@ class LidCgmAlertActivatedFsl2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCgmAlertClearedFsl2(BaseEvent):
@@ -4273,9 +4471,159 @@ class LidCgmAlertClearedFsl2(BaseEvent):
return self.raw.timestamp
@property
def eventId(self):
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidDailyBasal(BaseEvent):
"""81: LID_DAILY_BASAL"""
ID = 81
NAME = "LID_DAILY_BASAL"
raw: RawEvent
dailytotalbasal: float # units
lastbasalrate: float # units/hour
iob: float # units
batterychargepercentmsbRaw: int
batterychargepercentlsbRaw: int
batterylipomillivolts: int
@property
def batteryChargePercent(self):
return (256*(self.batterychargepercentmsbRaw%2)+self.batterychargepercentlsbRaw)/512
@staticmethod
def build(raw):
dailytotalbasal, = struct.unpack_from(FLOAT32, raw[:EVENT_LEN], 10)
lastbasalrate, = struct.unpack_from(FLOAT32, raw[:EVENT_LEN], 14)
iob, = struct.unpack_from(FLOAT32, raw[:EVENT_LEN], 18)
batterychargepercentmsbRaw, = struct.unpack_from(UINT8, raw[:EVENT_LEN], 22)
batterychargepercentlsbRaw, = struct.unpack_from(UINT8, raw[:EVENT_LEN], 23)
batterylipomillivolts, = struct.unpack_from(UINT16, raw[:EVENT_LEN], 24)
return LidDailyBasal(
raw = RawEvent.build(raw),
dailytotalbasal = dailytotalbasal,
lastbasalrate = lastbasalrate,
iob = iob,
batterychargepercentmsbRaw = batterychargepercentmsbRaw,
batterychargepercentlsbRaw = batterychargepercentlsbRaw,
batterylipomillivolts = batterylipomillivolts,
)
@property
def eventTimestamp(self):
return self.raw.timestamp
@property
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidCarbsEntered(BaseEvent):
"""48: LID_CARBS_ENTERED"""
ID = 48
NAME = "LID_CARBS_ENTERED"
raw: RawEvent
carbs: float # carbs
@staticmethod
def build(raw):
carbs, = struct.unpack_from(FLOAT32, raw[:EVENT_LEN], 10)
return LidCarbsEntered(
raw = RawEvent.build(raw),
carbs = carbs,
)
@property
def eventTimestamp(self):
return self.raw.timestamp
@property
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidUsbConnected(BaseEvent):
"""36: LID_USB_CONNECTED"""
ID = 36
NAME = "LID_USB_CONNECTED"
raw: RawEvent
negotiatedcurrent: float # mA
@staticmethod
def build(raw):
negotiatedcurrent, = struct.unpack_from(FLOAT32, raw[:EVENT_LEN], 10)
return LidUsbConnected(
raw = RawEvent.build(raw),
negotiatedcurrent = negotiatedcurrent,
)
@property
def eventTimestamp(self):
return self.raw.timestamp
@property
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
@dataclass
class LidUsbDisconnected(BaseEvent):
"""37: LID_USB_DISCONNECTED"""
ID = 37
NAME = "LID_USB_DISCONNECTED"
raw: RawEvent
negotiatedcurrent: float # mA
@staticmethod
def build(raw):
negotiatedcurrent, = struct.unpack_from(FLOAT32, raw[:EVENT_LEN], 10)
return LidUsbDisconnected(
raw = RawEvent.build(raw),
negotiatedcurrent = negotiatedcurrent,
)
@property
def eventTimestamp(self):
return self.raw.timestamp
@property
def seqNum(self):
return self.raw.seqNum
@property
def eventId(self):
return self.ID
EVENT_IDS = {
3: LidBasalRateChange,
4: LidAlertActivated,
@@ -4329,6 +4677,10 @@ EVENT_IDS = {
447: LidCgmStopSessionG7,
460: LidCgmAlertActivatedFsl2,
461: LidCgmAlertClearedFsl2,
81: LidDailyBasal,
48: LidCarbsEntered,
36: LidUsbConnected,
37: LidUsbDisconnected,
}
EVENT_NAMES = {
@@ -4384,5 +4736,9 @@ EVENT_NAMES = {
"LID_CGM_STOP_SESSION_G7": LidCgmStopSessionG7,
"LID_CGM_ALERT_ACTIVATED_FSL2": LidCgmAlertActivatedFsl2,
"LID_CGM_ALERT_CLEARED_FSL2": LidCgmAlertClearedFsl2,
"LID_DAILY_BASAL": LidDailyBasal,
"LID_CARBS_ENTERED": LidCarbsEntered,
"LID_USB_CONNECTED": LidUsbConnected,
"LID_USB_DISCONNECTED": LidUsbDisconnected,
}
+7 -1
View File
@@ -8,7 +8,13 @@ from .events import EVENT_IDS
from .utils import batched
Event = lambda x: EVENT_IDS[RawEvent.build(x).id].build(x)
def Event(x):
raw_event = RawEvent.build(x)
if not raw_event.id in EVENT_IDS:
return raw_event
return EVENT_IDS[raw_event.id].build(x)
Events = lambda x: (Event(bytearray(e)) for e in batched(x, EVENT_LEN))
def decode_raw_events(raw):
+33 -2
View File
@@ -1,6 +1,8 @@
import struct
import arrow
from ..secret import TIMEZONE_NAME
from dataclasses import dataclass
EVENT_LEN = 26
@@ -8,6 +10,21 @@ UINT16 = '>H'
UINT32 = '>I'
TANDEM_EPOCH = 1199145600
@dataclass
class BaseEvent:
@staticmethod
def build(raw):
raise NotImplemented
@property
def eventTimestamp(self):
raise NotImplemented
@property
def eventId(self):
raise NotImplemented
@dataclass
class RawEvent:
source: int
@@ -29,7 +46,21 @@ class RawEvent:
seqNum = seqNum,
raw = raw
)
@property
def timestamp(self):
return arrow.get(TANDEM_EPOCH + self.timestampRaw)
# Event timestamps do not have TZ data attached to them when parsed,
# but represent the user's time zone setting. So we keep the time
# referenced on them, but force the timezone to what the user
# requests via the TZ secret.
return arrow.get(TANDEM_EPOCH + self.timestampRaw, tzinfo='UTC').replace(tzinfo=TIMEZONE_NAME)
@property
def eventId(self):
return self.id
@property
def eventTimestamp(self):
return self.timestamp
+12 -1
View File
@@ -111,11 +111,22 @@ def transform_ratio(event_def, name, name_fmt, field, tx):
return out
def transform_battery_charge_percent(event_def, name, name_fmt, field, tx):
out = []
out += [
'@property',
f'def batteryChargePercent(self):',
f' return (256*(self.batterychargepercentmsbRaw%2)+self.batterychargepercentlsbRaw)/512',
''
]
return out
TRANSFORMS = {
'enum': transform_enum,
'dictionary': transform_dictionary,
'bitmask': transform_bitmask,
'ratio': transform_ratio
'ratio': transform_ratio,
'battery_charge_percent': transform_battery_charge_percent
}
+2
View File
@@ -10,6 +10,7 @@ PUMP_EVENTS = "PUMP_EVENTS"
PUMP_EVENTS_BASAL_SUSPENSION = "PUMP_EVENTS_BASAL_SUSPENSION"
PROFILES = "PROFILES"
CGM_ALERTS = "CGM_ALERTS"
DEVICE_STATUS = "DEVICE_STATUS"
DEFAULT_FEATURES = [
BASAL,
@@ -27,6 +28,7 @@ ALL_FEATURES = [
PROFILES,
CGM,
CGM_ALERTS,
DEVICE_STATUS,
]
+27
View File
@@ -154,6 +154,33 @@ class NightscoutApi:
else:
raise e
def last_uploaded_devicestatus(self, time_start=None, time_end=None):
def internal(t_to_space):
dateFilter = time_range('created_at', time_start, time_end, t_to_space=t_to_space)
latest = requests.get(urljoin(self.url, 'api/v1/devicestatus?find[device]=' + urllib.parse.quote(ENTERED_BY) + dateFilter + '&ts=' + str(time.time())), headers={
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
}, verify=self.verify)
if latest.status_code != 200:
raise ApiException(latest.status_code, "Nightscout devicestatus %s response: %s" % (latest.status_code, latest.text))
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
try:
ret = internal(False)
if ret is None and (time_start or time_end):
ret = internal(True)
if ret is not None:
logger.warning("devicestatus time_start=%s time_end=%s only returned data when timestamps contained a space" % (time_start, time_end))
return ret
except requests.exceptions.ConnectionError as e:
if self.ignore_conn_errors:
logger.warn('Ignoring ConnectionError because ignore_conn_errors=true', e)
else:
raise e
"""
Returns general status information about the Nightscout server.
"""
+25 -8
View File
@@ -194,6 +194,20 @@ class NightscoutEntry:
"pump_event_id": pump_event_id
}
@staticmethod
def devicestatus(created_at, batteryVoltage, batteryString, pump_event_id=""):
return {
"device": ENTERED_BY,
"created_at": created_at,
"pump": {
"battery": {
"voltage": float(batteryVoltage),
"string": batteryString
},
},
"pump_event_id": pump_event_id
}
# Tandem-scraped profile to Nightscout profile store entry
@staticmethod
def profile_store(profile: Profile, device_settings: DeviceSettings) -> dict:
@@ -251,30 +265,33 @@ class NightscoutEntry:
return {
# insulin duration in hours; Nightscout JS bug requires all top-level fields to be strings
"dia": "%s" % (profile.insulinDuration / 60),
"carbratio": [
"carbratio": list(sorted([
{
"time": minutes_to_ns_time(segment.startTime),
"timeAsSeconds": segment.startTime * 60,
"value": segment.carbRatio / 1000 # milliunits->units
} for segment in profile.tDependentSegs
],
} for segment in profile.tDependentSegs if not segment.skip
], key=lambda x: x["timeAsSeconds"])),
"carbs_hr": NIGHTSCOUT_PROFILE_CARBS_HR_VALUE,
"delay": NIGHTSCOUT_PROFILE_DELAY_VALUE,
"sens": [ # Correction factor / isf
"sens": list(sorted([ # Correction factor / isf
{
"time": minutes_to_ns_time(segment.startTime),
"timeAsSeconds": segment.startTime * 60,
"value": segment.isf
} for segment in profile.tDependentSegs
],
"basal": [
} for segment in profile.tDependentSegs if not segment.skip
], key=lambda x: x["timeAsSeconds"])),
"basal": list(sorted([
{
"time": minutes_to_ns_time(segment.startTime),
"timeAsSeconds": segment.startTime * 60,
"value": segment.basalRate / 1000 # milliunits->units
} for segment in profile.tDependentSegs
],
], key=lambda x: x["timeAsSeconds"])),
"target_low": [
{
"time": "00:00",
+5
View File
@@ -54,6 +54,7 @@ if not get('NS_SECRET') and get('API_SECRET'):
NS_SKIP_TLS_VERIFY = get_bool('NS_SKIP_TLS_VERIFY', 'false')
NS_IGNORE_CONN_ERRORS = get_bool('NS_IGNORE_CONN_ERRORS', 'false')
# This should be the timezone your pump is set to.
TIMEZONE_NAME = get('TIMEZONE_NAME', 'America/New_York')
if not get('TIMEZONE_NAME') and get('TZ'):
@@ -75,9 +76,13 @@ AUTOUPDATE_MAX_LOOP_INVOCATIONS = get_number('AUTOUPDATE_MAX_LOOP_INVOCATIONS',
NIGHTSCOUT_PROFILE_UPLOAD_MODE = get_one_of('NIGHTSCOUT_PROFILE_UPLOAD_MODE', 'add', ['add', 'replace'])
# When set, all possible history log event types are fetched from Tandem Source
FETCH_ALL_EVENT_TYPES = get_bool('FETCH_ALL_EVENT_TYPES', 'false')
# Default Nightscout profile segment fields which aren't stored by Tandem
NIGHTSCOUT_PROFILE_CARBS_HR_VALUE = get('NIGHTSCOUT_PROFILE_CARBS_HR_VALUE', '20')
NIGHTSCOUT_PROFILE_DELAY_VALUE = get('NIGHTSCOUT_PROFILE_DELAY_VALUE', '20')
IGNORE_ZERO_UNIT_BASAL = get_bool('IGNORE_ZERO_UNIT_BASAL', 'false')
ENABLE_TESTING_MODES = get_bool('ENABLE_TESTING_MODES', 'false')
SKIP_NS_LAST_UPLOADED_CHECK = get_bool('SKIP_NS_LAST_UPLOADED_CHECK', 'false')
+10 -5
View File
@@ -18,6 +18,7 @@ class TandemSourceAutoupdate:
self.last_max_date_with_events = None
self.last_event_time = 0
self.last_attempt_time = 0
self.last_event_seqnum = None
self.time_diffs_between_attempts = []
self.time_diffs_between_updates = []
@@ -41,26 +42,30 @@ class TandemSourceAutoupdate:
tconnectDevice = ChooseDevice(self.secret, tconnect).choose()
cur_max_date_with_events = arrow.get(tconnectDevice['maxDateWithEvents'])
if not self.last_max_date_with_events or cur_max_date_with_events > self.cur_max_date_with_events:
event_seqnum = None
cur_max_date_with_events = arrow.get(tconnectDevice['maxDateWithEvents']).float_timestamp
if not self.last_max_date_with_events or cur_max_date_with_events > self.last_max_date_with_events:
logger.info('New reported tandemsource data. (cur_max_date: %s last_max_date: %s)' % (cur_max_date_with_events, self.last_max_date_with_events))
if pretend:
logger.info('Would update now if not in pretend mode')
else:
added = ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, features=features).process(time_start, time_end)
added, event_seqnum = ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, self.secret, features=features).process(time_start, time_end)
logger.info('Added %d items from ProcessTimeRange' % added)
self.last_successful_process_time_range = now
# Track the time it took to find a new event between runs,
# but skip this calculation the first process cycle (since
# we don't know at what exact point the event index changed)
if self.last_event_index:
if self.last_event_seqnum:
self.time_diffs_between_updates.append(now - self.last_max_date_with_events)
logger.debug('Updating tracking of time since last update: %s' % self.time_diffs_between_updates)
# Mark the last event index uploaded from the pump and timestamp
if event_seqnum:
self.last_event_seqnum = event_seqnum
self.last_event_time = now
self.last_max_date_with_events = cur_max_date_with_events
self.last_event_time = now
self.last_attempt_time = now
self.time_diffs_between_attempts = []
else:
@@ -0,0 +1,20 @@
from ...features import DEFAULT_FEATURES
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from .choose_device import ChooseDevice
from .process import ProcessTimeRange
from ...api import TConnectApi
from ... import secret
import datetime
import logging
logger = logging.getLogger(__name__)
def fetch_oneshot(username, password, time_start=None, time_end=None):
tconnect = TConnectApi(username, password)
if not time_start and not time_end:
time_end = datetime.datetime.now()
time_start = time_end - datetime.timedelta(days=1)
tconnectDevice = ChooseDevice(secret, tconnect).choose()
return tconnect.tandemsource.pump_events(tconnectDevice['tconnectDeviceId'], time_start, time_end, fetch_all_event_types=secret.FETCH_ALL_EVENT_TYPES)
@@ -0,0 +1,17 @@
from ...features import DEFAULT_FEATURES
from .choose_device import ChooseDevice
from .process import ProcessTimeRange
from ... import secret
import datetime
def run_oneshot(tconnect, nightscout, pretend=False, features=DEFAULT_FEATURES, secret_arg=None, time_start=None, time_end=None):
if not time_start and not time_end:
time_end = datetime.datetime.now()
time_start = time_end - datetime.timedelta(days=1)
if not secret_arg:
secret_arg = secret
tconnectDevice = ChooseDevice(secret_arg, tconnect).choose()
return ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, secret_arg, features).process(time_start, time_end)
+12 -10
View File
@@ -2,7 +2,6 @@ import logging
import collections
from ...features import DEFAULT_FEATURES
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from .process_basal import ProcessBasal
@@ -20,12 +19,13 @@ from .update_profiles import UpdateProfiles
logger = logging.getLogger(__name__)
class ProcessTimeRange:
def __init__(self, tconnect, nightscout, tconnectDevice, pretend, features=DEFAULT_FEATURES):
def __init__(self, tconnect, nightscout, tconnectDevice, pretend, secret, features=DEFAULT_FEATURES):
self.tconnect = tconnect
self.nightscout = nightscout
self.tconnect_device_id = tconnectDevice['tconnectDeviceId']
self.max_date_with_events = tconnectDevice['maxDateWithEvents']
self.pretend = pretend
self.secret = secret
self.features = features
event_classes = {
@@ -47,22 +47,22 @@ class ProcessTimeRange:
def process(self, time_start, time_end):
logger.info(f"ProcessTimeRange time_start={time_start} time_end={time_end} tconnect_device_id={self.tconnect_device_id} features={self.features}")
pump_events_raw = self.tconnect.tandemsource.pump_events_raw(self.tconnect_device_id, time_start, time_end)
pump_events_decoded = decode_raw_events(pump_events_raw)
logger.info(f"Read {len(pump_events_decoded)} bytes (est. {len(pump_events_decoded)/EVENT_LEN} events)")
events = Events(pump_events_decoded)
events = self.tconnect.tandemsource.pump_events(self.tconnect_device_id, time_start, time_end, fetch_all_event_types=self.secret.FETCH_ALL_EVENT_TYPES)
events_first_time = None
events_last_time = None
last_event_seqnum = None
for_eventclass = collections.defaultdict(list)
for event in events:
if not events_first_time:
events_first_time = event.eventTimestamp
if not events_last_time:
events_last_time = event.eventTimestamp
if not last_event_seqnum:
last_event_seqnum = event.seqNum
events_first_time = min(events_first_time, event.eventTimestamp)
events_last_time = max(events_last_time, event.eventTimestamp)
last_event_seqnum = max(event.seqNum, last_event_seqnum)
clazz = EventClass.for_event(event)
if clazz:
@@ -78,7 +78,9 @@ class ProcessTimeRange:
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)
w = c.write(ns_entries)
if w:
processed_count += w
else:
logger.info("Skipping %s, is not enabled from features %s" % (clazz, self.features))
@@ -91,6 +93,6 @@ class ProcessTimeRange:
else:
logger.info("Skipping %s, is not enabled from features %s" % (updater_class.__name__, self.features))
logger.info("Processed %d events" % processed_count)
return processed_count
logger.info("Processed %d events. Last event ID seen: %d" % (processed_count if processed_count else 0, last_event_seqnum if last_event_seqnum else -1))
return processed_count, last_event_seqnum
@@ -35,9 +35,12 @@ class ProcessAlarm:
ns_entries = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
if self.pretend:
logger.info("Skipping Alarm event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
logger.info("Skipping Alarm event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
if self.skip_event(event):
continue
ns_entries.append(self.alarm_to_nsentry(event))
@@ -45,6 +48,12 @@ class ProcessAlarm:
return ns_entries
def skip_event(self, event):
return event.alarmid in (
eventtypes.LidAlarmActivated.AlarmidEnum.ResumePumpAlarm,
eventtypes.LidAlarmActivated.AlarmidEnum.ResumePumpAlarm2
)
def write(self, ns_entries):
count = 0
for entry in ns_entries:
@@ -63,11 +72,11 @@ class ProcessAlarm:
return NightscoutEntry.alarm(
created_at = event.eventTimestamp.format(),
reason = "%s" % event.alarmid.name,
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
elif type(event) == eventtypes.LidMalfunctionActivated:
return NightscoutEntry.alarm(
created_at = event.eventTimestamp.format(),
reason = "Malfunction",
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
@@ -1,6 +1,7 @@
import logging
import arrow
from ...secret import IGNORE_ZERO_UNIT_BASAL
from ...features import DEFAULT_FEATURES
from ... import features
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
@@ -36,13 +37,17 @@ class ProcessBasal:
with_duration = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
if self.pretend:
logger.info("Skipping basal event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
logger.info("Skipping basal event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
with_duration.append([event.eventTimestamp, None, event])
if not with_duration:
logger.info("No basal events found to process")
return []
for i in range(len(with_duration)-1):
with_duration[i][1] = with_duration[i+1][0] - with_duration[i][0]
@@ -50,7 +55,9 @@ class ProcessBasal:
ns_entries = []
for item in with_duration:
ns_entries.append(self.basal_to_nsentry(*item))
ns = self.basal_to_nsentry(*item)
if ns:
ns_entries.append(ns)
return ns_entries
@@ -69,18 +76,26 @@ class ProcessBasal:
def basal_to_nsentry(self, start, duration, event):
if type(event) == eventtypes.LidBasalRateChange:
value = insulin_float_round(event.commandedbasalrate)
if IGNORE_ZERO_UNIT_BASAL and value < 0.01:
logger.info("Ignoring basal entry with %.2f unit basal because IGNORE_ZERO_UNIT_BASAL=true: %s" % (value, event))
return None
return NightscoutEntry.basal(
value = insulin_float_round(event.commandedbasalrate),
value = value,
duration_mins = duration.seconds / 60,
created_at = start.format(),
reason = ', '.join(bitmask_to_list(event.changetype)),
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
if type(event) == eventtypes.LidBasalDelivery:
value = insulin_milliunits_to_real(event.commandedRate)
if IGNORE_ZERO_UNIT_BASAL and value < 0.01:
logger.info("Ignoring basal entry with %.2f unit basal because IGNORE_ZERO_UNIT_BASAL=true: %s" % (value, event))
return None
return NightscoutEntry.basal(
value = insulin_milliunits_to_real(event.commandedRate),
value = value,
duration_mins = duration.seconds / 60,
created_at = start.format(),
reason = ', '.join(bitmask_to_list(event.commandedRateSource)),
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
@@ -35,9 +35,9 @@ class ProcessBasalResume:
ns_entries = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
if self.pretend:
logger.info("Skipping BasalResume event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
logger.info("Skipping BasalResume event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
ns_entries.append(self.resume_to_nsentry(event))
@@ -62,5 +62,5 @@ class ProcessBasalResume:
if type(event) == eventtypes.LidPumpingResumed:
return NightscoutEntry.basalresume(
created_at = event.eventTimestamp.format(),
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
@@ -35,9 +35,9 @@ class ProcessBasalSuspension:
ns_entries = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
if self.pretend:
logger.info("Skipping basalsuspension event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
logger.info("Skipping basalsuspension event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
ns_entries.append(self.suspension_to_nsentry(event))
@@ -63,5 +63,5 @@ class ProcessBasalSuspension:
return NightscoutEntry.basalsuspension(
created_at = event.eventTimestamp.format(),
reason = ', '.join(bitmask_to_list(event.suspendreason)),
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
@@ -44,9 +44,9 @@ class ProcessBolus:
bolusEventsForId[event.bolusid][type(event)] = event
if type(event) == eventtypes.LidBolusCompleted:
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
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))
logger.info("Skipping bolusCompletedEvent not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
bolusCompletedEvents.append(event)
@@ -91,10 +91,10 @@ class ProcessBolus:
suffix = (' ' + (' '.join(suffixes))) if suffixes else ''
event_ids = []
seq_nums = []
for e in [bolusCompleted, bolusRequested1, bolusRequested2, bolusRequested3]:
if e:
event_ids.append(str(e.eventId))
seq_nums.append(str(e.seqNum))
notes = ''
if bolusRequested2 and str(bolusRequested2.optionsRaw) in eventtypes.LidBolusRequestedMsg2.OptionsMap:
@@ -107,6 +107,6 @@ class ProcessBolus:
created_at = bolusCompleted.eventTimestamp.format(),
notes = notes + suffix,
bg = bolusRequested1.BG if bolusRequested1 and bolusRequested1.BG > 0 else None,
pump_event_id = ",".join(event_ids)
pump_event_id = ",".join(seq_nums)
)
@@ -37,9 +37,9 @@ class ProcessCartridge:
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 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))
logger.info("Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
if type(event) == eventtypes.LidCartridgeFilled:
@@ -82,19 +82,19 @@ class ProcessCartridge:
return NightscoutEntry.sitechange(
created_at = cartFilled.eventTimestamp.format(),
reason = "Cartridge Filled" + (" (%du filled)" % round(cartFilled.v2Volume) if cartFilled.v2Volume else ""),
pump_event_id = "%s" % cartFilled.eventId
pump_event_id = "%s" % cartFilled.seqNum
)
def cannula_to_nsentry(self, cannulaFilled):
return NightscoutEntry.sitechange(
created_at = cannulaFilled.eventTimestamp.format(),
reason = "Cannula Filled" + (" (%du primed)" % round(cannulaFilled.primesize, 2) if cannulaFilled.primesize else ""),
pump_event_id = "%s" % cannulaFilled.eventId
pump_event_id = "%s" % cannulaFilled.seqNum
)
def tubing_to_nsentry(self, tubingFilled):
return NightscoutEntry.sitechange(
created_at = tubingFilled.eventTimestamp.format(),
reason = "Tubing Filled" + (" (%du primed)" % round(tubingFilled.primesize) if tubingFilled.primesize else ""),
pump_event_id = "%s" % tubingFilled.eventId
pump_event_id = "%s" % tubingFilled.seqNum
)
@@ -35,9 +35,9 @@ class ProcessCGMAlert:
alertEvents = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
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))
logger.info("Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
alertEvents.append(event)
@@ -73,7 +73,7 @@ class ProcessCGMAlert:
return NightscoutEntry.cgm_alert(
created_at = alert.eventTimestamp.format(),
reason = ("CGM Alert (%s)" % alert.dalertid.name) if alert.dalertid else "CGM Alert (Unknown)",
pump_event_id = "%s" % alert.eventId
pump_event_id = "%s" % alert.seqNum
)
elif type(alert) == eventtypes.LidCgmAlertActivatedDex:
if alert.dalertid == eventtypes.LidCgmAlertActivatedDex.DalertidEnum.CgmOutOfRange:
@@ -82,11 +82,11 @@ class ProcessCGMAlert:
return NightscoutEntry.cgm_alert(
created_at = alert.eventTimestamp.format(),
reason = ("Dexcom CGM Alert (%s)" % alert.dalertid.name) if alert.dalertid else "Dexcom CGM Alert (Unknown)",
pump_event_id = "%s" % alert.eventId
pump_event_id = "%s" % alert.seqNum
)
elif type(alert) == eventtypes.LidCgmAlertActivatedFsl2:
return NightscoutEntry.cgm_alert(
created_at = alert.eventTimestamp.format(),
reason = ("Libre CGM Alert (%s)" % alert.dalertid.name) if alert.dalertid else "Libre CGM Alert (Unknown)",
pump_event_id = "%s" % alert.eventId
pump_event_id = "%s" % alert.seqNum
)
@@ -38,9 +38,9 @@ class ProcessCGMReading:
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 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))
logger.info("ProcessCGMReading: Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
readings.append(event)
@@ -72,5 +72,5 @@ class ProcessCGMReading:
return NightscoutEntry.entry(
sgv = event.currentglucosedisplayvalue,
created_at = self.timestamp_for(event).format(),
pump_event_id = "%s" % event.eventId,
pump_event_id = "%s" % event.seqNum,
)
@@ -48,9 +48,9 @@ class ProcessCGMStartJoinStop:
allEvents = []
for event in sorted(events, key=lambda x: x.eventTimestamp):
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
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))
logger.info("ProcessCGMStartJoinStop: Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
allEvents.append(event)
@@ -80,17 +80,17 @@ class ProcessCGMStartJoinStop:
return NightscoutEntry.cgm_start(
created_at = event.eventTimestamp.format(),
reason = "CGM Session Started",
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
elif type(event) in EventClass._CGM_JOIN:
return NightscoutEntry.cgm_join(
created_at = event.eventTimestamp.format(),
reason = "CGM Session Joined",
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
elif type(event) in EventClass._CGM_STOP:
return NightscoutEntry.cgm_stop(
created_at = event.eventTimestamp.format(),
reason = "CGM Session Stopped",
pump_event_id = "%s" % event.eventId
pump_event_id = "%s" % event.seqNum
)
@@ -0,0 +1,77 @@
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
)
logger = logging.getLogger(__name__)
class ProcessDeviceStatus:
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.DEVICE_STATUS in self.features
def process(self, events, time_start, time_end):
logger.debug("ProcessDeviceStatus: querying for last uploaded devicestatus")
last_upload = self.nightscout.last_uploaded_devicestatus(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("ProcessDeviceStatus: Last Nightscout devicestatus upload: %s" % last_upload_time)
last_daily_basal_event = None
for event in sorted(events, key=lambda x: x.raw.timestamp):
if last_upload_time and event.raw.timestamp <= last_upload_time:
if self.pretend:
logger.info("ProcessDeviceStatus: Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
continue
if isinstance(event, eventtypes.LidDailyBasal):
last_daily_basal_event = event
if not last_daily_basal_event:
logger.info("ProcessDeviceStatus: No last_daily_basal_event found for add (time range: %s - %s)" % (time_start, time_end))
return []
ns_entries = []
ns_entries.append(self.daily_basal_to_nsentry(last_daily_basal_event))
return ns_entries
def daily_basal_to_nsentry(self, event):
return NightscoutEntry.devicestatus(
created_at=event.eventTimestamp.format(),
batteryVoltage=(float(event.batterylipomillivolts or 0)/1000),
batteryString="%.0f%s" % (100*event.batteryChargePercent, '%'),
pump_event_id = "%s" % event.seqNum
)
def write(self, ns_entries):
count = 0
for entry in ns_entries:
if self.pretend:
logger.info("Would upload devicestatus to Nightscout: %s" % entry)
else:
logger.info("Uploading devicestatus to Nightscout: %s" % entry)
self.nightscout.upload_entry(entry, entity='devicestatus')
count += 1
return count
@@ -72,9 +72,9 @@ class ProcessUserMode:
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 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))
logger.info("ProcessUserMode: Skipping usermode event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
continue
if self.is_start_sleep(event):
@@ -157,7 +157,7 @@ class ProcessUserMode:
reason=reason,
duration=duration_mins,
event_type=SLEEP_EVENTTYPE,
pump_event_id = "%s,%s" % (start.eventId, stop.eventId)
pump_event_id = "%s,%s" % (start.seqNum, stop.seqNum)
)
elif start:
reason = None
@@ -172,7 +172,7 @@ class ProcessUserMode:
reason=reason + " - " + NOT_ENDED if reason else NOT_ENDED,
duration=duration_mins,
event_type=SLEEP_EVENTTYPE,
pump_event_id = "%s" % start.eventId
pump_event_id = "%s" % start.seqNum
)
@@ -191,20 +191,20 @@ class ProcessUserMode:
reason=reason,
duration=duration_mins,
event_type=EXERCISE_EVENTTYPE,
pump_event_id = "%s,%s" % (start.eventId, stop.eventId)
pump_event_id = "%s,%s" % (start.seqNum, stop.seqNum)
)
elif start:
reason = "Exercise"
if start.exercisechoice == eventtypes.LidAaUserModeChange.ExercisechoiceEnum.Timed:
reason = "Exercise (Timed)"
duration_mins = (time_end.eventTimestamp - start.eventTimestamp).seconds / 60
duration_mins = (time_end - start.eventTimestamp).seconds / 60
return NightscoutEntry.activity(
created_at=start.eventTimestamp.format(),
reason=reason + " - " + NOT_ENDED,
duration=duration_mins,
event_type=EXERCISE_EVENTTYPE,
pump_event_id = "%s,%s" % (start.eventId, stop.eventId)
pump_event_id = "%s" % start.seqNum
)
def process_unended_sleep_stop(self, event, sleep_last_upload):
@@ -220,7 +220,7 @@ class ProcessUserMode:
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)
pump_event_id="%s,%s" % (sleep_last_upload.get("pump_event_id",""), event.seqNum)
)
def process_unended_exercise_stop(self, event, exercise_last_upload):
@@ -240,5 +240,5 @@ class ProcessUserMode:
reason=reason,
duration=duration_mins,
event_type=EXERCISE_EVENTTYPE,
pump_event_id="%s,%s" % (exercise_last_upload.get("pump_event_id",""), event.eventId)
pump_event_id="%s,%s" % (exercise_last_upload.get("pump_event_id",""), event.seqNum)
)
@@ -8,7 +8,7 @@ from ...features import DEFAULT_FEATURES
from ... import features
from ...domain.tandemsource.pump_settings import PumpSettings
from ...parser.nightscout import (
NightscoutEntry
NightscoutEntry, ENTERED_BY
)
from ...secret import NIGHTSCOUT_PROFILE_UPLOAD_MODE
@@ -53,6 +53,8 @@ class UpdateProfiles:
if ns_profile_obj is None:
ns_profile_obj = {}
logger.info("Current Nightscout profile was authored by: %s" % (ns_profile_obj.get('enteredBy')))
diff, ns_profile_new = self.compare_profiles(pump_settings, ns_profile_obj)
if not diff:
logger.info("Pump and Nightscout profiles up to date")
@@ -145,6 +147,7 @@ class UpdateProfiles:
return False, ns_profile_obj
logger.info("New Nightscout profile object: %s", new_ns_profile)
new_ns_profile['enteredBy'] = ENTERED_BY
return True, new_ns_profile
def nightscout_profiles_identical(self, configured: dict, translated: dict) -> bool:
+3
View File
@@ -26,6 +26,9 @@ class NightscoutApi(tconnectsync.nightscout.NightscoutApi):
def last_uploaded_activity(self, activityType):
raise NotImplementedError
def last_uploaded_devicestatus(self, time_start=None, time_end=None):
raise NotImplementedError
def api_status(self):
raise NotImplementedError
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import unittest
import arrow
from tconnectsync.sync.tandemsource.process_alarm import ProcessAlarm
from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.generic import Event
from ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi
class TestProcessAlarm(unittest.TestCase):
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.nightscout = NightscoutApi()
self.tconnect_device_id = 'abcdef'
self.process = ProcessAlarm(self.tconnect, self.nightscout, self.tconnect_device_id, pretend=False)
def test_resume_alarm_ignored(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-11-11 07:24:29-05:00
Event(b'\x00\x05\x1f\xb8.\xad\x00\x0e\x91\xee\x00\x00\x00\x12\x00\x00 w\x01N\x0b\x16\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAlarmActivated)
self.assertEqual(events[0].alarmid, eventtypes.LidAlarmActivated.AlarmidEnum.ResumePumpAlarm)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 0)
def test_empty_cartridge_alarm(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-11-17 08:44:17-05:00
Event(b'\x00\x05\x1f\xc0*a\x00\x0e\xf5\x90\x00\x00\x00\x08\x00\x00 1\x00\x00\x00gA\x1a\x1e\x84')
]
self.assertEqual(type(events[0]), eventtypes.LidAlarmActivated)
self.assertEqual(events[0].alarmid, eventtypes.LidAlarmActivated.AlarmidEnum.EmptyCartridgeAlarm)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
'eventType': 'Alarm',
'created_at': '2024-11-17 08:44:17-05:00',
'enteredBy': 'Pump (tconnectsync)',
'notes': 'EmptyCartridgeAlarm',
'reason': 'EmptyCartridgeAlarm',
'pump_event_id': '980368'
})
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
import unittest
from tconnectsync.sync.tandemsource.process_device_status import ProcessDeviceStatus
from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.generic import Event
from ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi
class TestProcessDeviceStatus(unittest.TestCase):
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.nightscout = NightscoutApi()
self.tconnect_device_id = 'abcdef'
self.process = ProcessDeviceStatus(self.tconnect, self.nightscout, self.tconnect_device_id, pretend=False)
def test_single_event_no_last_uploaded(self):
self.nightscout.last_uploaded_devicestatus = lambda *args, **kwargs: None
events = [
Event(b'\x00Q\x1f\xd6\x14g\x00\x0f\xf7\xa4A\xb2\xd3\xe2?L\xcc\xcd@~\xdeb\x0e\xf67\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidDailyBasal)
self.assertEqual(events[0].batterychargepercentmsbRaw, 14)
self.assertEqual(events[0].batterychargepercentlsbRaw, 246)
self.assertEqual(events[0].batterylipomillivolts, 14080)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
'created_at': '2024-12-03 23:40:23-05:00',
'device': 'Pump (tconnectsync)',
'pump': {
'battery': {
'string': '48%',
'voltage': 14.08
}
},
'pump_event_id': '1046436'
})
def test_single_event_already_uploaded(self):
self.nightscout.last_uploaded_devicestatus = lambda *args, **kwargs: {'created_at': '2024-12-03 23:40:23-05:00'}
events = [
Event(b'\x00Q\x1f\xd6\x14g\x00\x0f\xf7\xa4A\xb2\xd3\xe2?L\xcc\xcd@~\xdeb\x0e\xf67\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidDailyBasal)
self.assertEqual(events[0].batterychargepercentmsbRaw, 14)
self.assertEqual(events[0].batterychargepercentlsbRaw, 246)
self.assertEqual(events[0].batterylipomillivolts, 14080)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 0)
def test_multiple_event_with_last_uploaded(self):
self.nightscout.last_uploaded_devicestatus = lambda *args, **kwargs: {'created_at': '2024-12-03 23:40:23-05:00'}
events = [
Event(b'\x00Q\x1f\xd6\x14g\x00\x0f\xf7\xa4A\xb2\xd3\xe2?L\xcc\xcd@~\xdeb\x0e\xf67\x00'),
Event(b'\x00Q\x1f\xd6<?\x00\x0f\xf9[@\r\xcd{?\x9b\xa5\xe3?\xe3\x9a;\x0e\xf36\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidDailyBasal)
self.assertEqual(events[0].raw.timestampRaw, 534123623) # 2024-12-03 23:40:23-05:00
self.assertEqual(events[0].batterychargepercentmsbRaw, 14)
self.assertEqual(events[0].batterychargepercentlsbRaw, 246)
self.assertEqual(events[0].batterylipomillivolts, 14080)
self.assertEqual(type(events[1]), eventtypes.LidDailyBasal)
self.assertEqual(events[1].raw.timestampRaw, 534133823) # 2024-12-04 02:30:23-05:00
self.assertEqual(events[1].batterychargepercentmsbRaw, 14)
self.assertEqual(events[1].batterychargepercentlsbRaw, 243)
self.assertEqual(events[1].batterylipomillivolts, 13824)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
'created_at': '2024-12-04 02:30:23-05:00',
'device': 'Pump (tconnectsync)',
'pump': {
'battery': {
'string': '47%',
'voltage': 13.824
}
},
'pump_event_id': '1046875'
})
def test_multiple_event_only_latest_applied(self):
self.nightscout.last_uploaded_devicestatus = lambda *args, **kwargs: {'created_at': '2024-12-03 23:40:23-05:00'}
events = [
Event(b'\x00Q\x1f\xd6\x14g\x00\x0f\xf7\xa4A\xb2\xd3\xe2?L\xcc\xcd@~\xdeb\x0e\xf67\x00'),
Event(b'\x00Q\x1f\xd6<?\x00\x0f\xf9[@\r\xcd{?\x9b\xa5\xe3?\xe3\x9a;\x0e\xf36\x00'),
Event(b'\x00Q\x1f\xd6k\x1f\x00\x0f\xfc>A\x0e\xa3\x80?\x9a~\xfa@\x11z6\x0e\xee5\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidDailyBasal)
self.assertEqual(events[0].raw.timestampRaw, 534123623) # 2024-12-03 23:40:23-05:00
self.assertEqual(events[0].batterychargepercentmsbRaw, 14)
self.assertEqual(events[0].batterychargepercentlsbRaw, 246)
self.assertEqual(events[0].batterylipomillivolts, 14080)
self.assertEqual(type(events[1]), eventtypes.LidDailyBasal)
self.assertEqual(events[1].raw.timestampRaw, 534133823) # 2024-12-04 02:30:23-05:00
self.assertEqual(events[1].batterychargepercentmsbRaw, 14)
self.assertEqual(events[1].batterychargepercentlsbRaw, 243)
self.assertEqual(events[1].batterylipomillivolts, 13824)
self.assertEqual(type(events[2]), eventtypes.LidDailyBasal)
self.assertEqual(events[2].raw.timestampRaw, 534145823) # 2024-12-04 05:50:23-05:00
self.assertEqual(events[2].batterychargepercentmsbRaw, 14)
self.assertEqual(events[2].batterychargepercentlsbRaw, 238)
self.assertEqual(events[2].batterylipomillivolts, 13568)
p = self.process.process(events, time_start=None, time_end=None)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
'created_at': '2024-12-04 05:50:23-05:00',
'device': 'Pump (tconnectsync)',
'pump': {
'battery': {
'string': '46%',
'voltage': 13.568
}
},
'pump_event_id': '1047614'
})
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
import unittest
import arrow
from tconnectsync.sync.tandemsource.process_user_mode import ProcessUserMode
from tconnectsync.eventparser import events as eventtypes
from tconnectsync.eventparser.generic import Event
from ...api.fake import TConnectApi
from ...nightscout_fake import NightscoutApi
class TestProcessUserModeSleep(unittest.TestCase):
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.nightscout = NightscoutApi()
self.tconnect_device_id = 'abcdef'
self.process = ProcessUserMode(self.tconnect, self.nightscout, self.tconnect_device_id, pretend=False)
def test_single_start_sleep_active(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-12-04 23:00:23-05:00 - sleep start
Event(b'\x00\xe5\x1f\xd7\\\x87\x00\x10\t\xaa\x00\x01\x00\x01\x00\x00\x01\x00\x00\xf0\x01\x01\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Normal)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Sleeping)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StartSleep)
time_end = arrow.get('2024-12-04T23:00:30-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
"eventType": 'Sleep',
"reason": 'Sleep (Scheduled) - Not Ended',
"notes": 'Sleep (Scheduled) - Not Ended',
"duration": 0.11666666666666667,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050'
})
self.assertEqual(self.nightscout.deleted_entries, [])
def test_single_stop_sleep_no_last_uploaded(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-12-04 09:01:23-05:00 - sleep end
Event(b'\x00\xe5\x1f\xd6\x97\xe3\x00\x0f\xfe\xb0\x00\x02\x01\x00\x00\x00\x00\x00\x00\xf0\x01\x01\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Sleeping)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Normal)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StopSleep)
time_end = arrow.get('2024-12-04T23:00:30-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 0)
self.assertEqual(self.nightscout.deleted_entries, [])
def test_start_and_stop_sleep(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-12-04 23:00:23-05:00 - sleep start
Event(b'\x00\xe5\x1f\xd7\\\x87\x00\x10\t\xaa\x00\x01\x00\x01\x00\x00\x01\x00\x00\xf0\x01\x01\x00\x00\x00\x00'),
# 2024-12-05 09:01:23-05:00 - sleep end
Event(b'\x00\xe5\x1f\xd7\xe9c\x00\x10\x10\xf8\x00\x02\x01\x00\x00\x00\x00\x00\x00\xf0\x01\x01\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Normal)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Sleeping)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StartSleep)
self.assertEqual(type(events[1]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[1].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Sleeping)
self.assertEqual(events[1].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Normal)
self.assertEqual(events[1].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StopSleep)
time_end = arrow.get('2024-12-05T10:00:00-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
"eventType": 'Sleep',
"reason": 'Sleep (Scheduled)',
"notes": 'Sleep (Scheduled)',
"duration": 601.0,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050,1052920'
})
self.assertEqual(self.nightscout.deleted_entries, [])
def test_stop_with_partial_last_uploaded(self):
self.nightscout.last_uploaded_entry = lambda eventType, **kwargs: {
'Sleep': {
"eventType": 'Sleep',
"reason": 'Sleep (Scheduled) - Not Ended',
"notes": 'Sleep (Scheduled) - Not Ended',
"duration": 0.11666666666666667,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050',
"_id": "id_to_delete"
}
}.get(eventType)
events = [
# 2024-12-05 09:01:23-05:00 - sleep end
Event(b'\x00\xe5\x1f\xd7\xe9c\x00\x10\x10\xf8\x00\x02\x01\x00\x00\x00\x00\x00\x00\xf0\x01\x01\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Sleeping)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Normal)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StopSleep)
time_end = arrow.get('2024-12-05T10:00:00-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
"eventType": 'Sleep',
"reason": 'Sleep (Scheduled)',
"notes": 'Sleep (Scheduled)',
"duration": 601.0,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050,1052920'
})
self.assertEqual(self.nightscout.deleted_entries, ['treatments/id_to_delete'])
class TestProcessUserModeExercise(unittest.TestCase):
maxDiff = None
def setUp(self):
self.tconnect = TConnectApi()
self.nightscout = NightscoutApi()
self.tconnect_device_id = 'abcdef'
self.process = ProcessUserMode(self.tconnect, self.nightscout, self.tconnect_device_id, pretend=False)
def test_single_start_exercise_active(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-12-04 23:00:23-05:00 - exercise start
Event(b'\x00\xe5\x1f\xd7\\\x87\x00\x10\t\xaa\x00\x03\x00\x02\x00\x00\x01\x00\x00\xf0\x00\x01\x00\x00\x00\x00'),
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Normal)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Exercising)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StartExercise)
time_end = arrow.get('2024-12-04T23:00:30-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
"eventType": 'Exercise',
"reason": 'Exercise - Not Ended',
"notes": 'Exercise - Not Ended',
"duration": 0.11666666666666667,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050'
})
self.assertEqual(self.nightscout.deleted_entries, [])
def test_single_stop_exercise_no_last_uploaded(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-12-04 09:01:23-05:00 - exercise end
Event(b'\x00\xe5\x1f\xd6\x97\xe3\x00\x0f\xfe\xb0\x00\x04\x02\x00\x00\x00\x00\x00\x00\xf0\x01\x03\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Exercising)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Normal)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StopExercise)
time_end = arrow.get('2024-12-04T23:00:30-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 0)
self.assertEqual(self.nightscout.deleted_entries, [])
def test_start_and_stop_exercise(self):
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
events = [
# 2024-12-04 23:00:23-05:00 - exercise start
Event(b'\x00\xe5\x1f\xd7\\\x87\x00\x10\t\xaa\x00\x03\x00\x02\x00\x00\x01\x00\x00\xf0\x00\x01\x00\x00\x00\x00'),
# 2024-12-05 09:01:23-05:00 - exercise end
Event(b'\x00\xe5\x1f\xd7\xe9c\x00\x10\x10\xf8\x00\x04\x02\x00\x00\x00\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Normal)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Exercising)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StartExercise)
self.assertEqual(type(events[1]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[1].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Exercising)
self.assertEqual(events[1].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Normal)
self.assertEqual(events[1].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StopExercise)
time_end = arrow.get('2024-12-05T10:00:00-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
"eventType": 'Exercise',
"reason": 'Exercise',
"notes": 'Exercise',
"duration": 601.0,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050,1052920'
})
self.assertEqual(self.nightscout.deleted_entries, [])
def test_stop_with_partial_last_uploaded(self):
self.nightscout.last_uploaded_entry = lambda eventType, **kwargs: {
'Exercise': {
"eventType": 'Exercise',
"reason": 'Exercise - Not Ended',
"notes": 'Exercise - Not Ended',
"duration": 0.11666666666666667,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050',
"_id": 'id_to_delete'
}
}.get(eventType)
events = [
# 2024-12-05 09:01:23-05:00 - exercise end
Event(b'\x00\xe5\x1f\xd7\xe9c\x00\x10\x10\xf8\x00\x04\x02\x00\x00\x00\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00')
]
self.assertEqual(type(events[0]), eventtypes.LidAaUserModeChange)
self.assertEqual(events[0].previoususermode, eventtypes.LidAaUserModeChange.PrevioususermodeEnum.Exercising)
self.assertEqual(events[0].currentusermode, eventtypes.LidAaUserModeChange.CurrentusermodeEnum.Normal)
self.assertEqual(events[0].requestedaction, eventtypes.LidAaUserModeChange.RequestedactionEnum.StopExercise)
time_end = arrow.get('2024-12-05T10:00:00-05:00')
p = self.process.process(events, time_start=None, time_end=time_end)
self.assertEqual(len(p), 1)
self.assertDictEqual(p[0], {
"eventType": 'Exercise',
"reason": 'Exercise',
"notes": 'Exercise',
"duration": 601.0,
"created_at": '2024-12-04 23:00:23-05:00',
"enteredBy": "Pump (tconnectsync)",
"pump_event_id": '1051050,1052920'
})
self.assertEqual(self.nightscout.deleted_entries, ['treatments/id_to_delete'])
if __name__ == '__main__':
unittest.main()