mirror of
https://github.com/jwoglom/tconnectsync.git
synced 2026-08-25 02:34:14 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
803b15b886 | ||
|
|
9e78fe770f | ||
|
|
dfb39e07ef | ||
|
|
9fad0d71ef | ||
|
|
f5cd7c0151 | ||
|
|
13bb106924 | ||
|
|
fd96b08aab | ||
|
|
4a8da39b44 | ||
|
|
7a0f9477f2 | ||
|
|
934d90e3b9 | ||
|
|
ea84b0b9cc | ||
|
|
07aaea9e1c | ||
|
|
67b9f42f9b | ||
|
|
997ab7bac8 | ||
|
|
b1f8263ceb | ||
|
|
29d08a7f1b | ||
|
|
c7ade49eea | ||
|
|
1f0fbd7788 | ||
|
|
4bda0a21f2 | ||
|
|
d1341e09c0 | ||
|
|
fb9588ff2c | ||
|
|
2339b4543e | ||
|
|
3f6010fafc | ||
|
|
a028cdf3fe | ||
|
|
093b42ff94 | ||
|
|
00023c6aba | ||
|
|
1ed3d35965 | ||
|
|
9493b6546f | ||
|
|
24a635ca21 | ||
|
|
80824906d9 | ||
|
|
e5259a7981 | ||
|
|
650fb60988 | ||
|
|
2b367d45f3 | ||
|
|
dd2dcd17bd | ||
|
|
98b7edc904 | ||
|
|
e82d311fd6 | ||
|
|
9a2a4ca771 | ||
|
|
54ba2ed7df | ||
|
|
2892eb8ace | ||
|
|
1d432982ad | ||
|
|
891c2e2c10 | ||
|
|
7dc479c84b | ||
|
|
0417fb9fd8 |
+2
-2
@@ -5,9 +5,9 @@ coverage:
|
||||
project:
|
||||
default: false
|
||||
tconnectsync:
|
||||
paths:
|
||||
paths:
|
||||
- "tconnectsync/"
|
||||
target: '75%'
|
||||
target: '60%'
|
||||
threshold: '5%'
|
||||
tests:
|
||||
paths:
|
||||
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
|
||||
with:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -28,25 +28,25 @@ jobs:
|
||||
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: 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
|
||||
|
||||
+3
-1
@@ -8,4 +8,6 @@ build
|
||||
*.egg-info
|
||||
.env
|
||||
tconnectsync-check-output.log
|
||||
ignore_*
|
||||
ignore_*
|
||||
.venv/
|
||||
.vscode/
|
||||
@@ -27,3 +27,4 @@ typing-extensions = "*"
|
||||
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'"
|
||||
@@ -1,6 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 50%
|
||||
threshold: null
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 2.1.3
|
||||
version = 2.2.4
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem Source (formerly t:connect) insulin pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -120,7 +120,7 @@ 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, last_event_id = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, features=args.features).process(time_start, time_end)
|
||||
added, last_event_id = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, secret=secret, features=args.features).process(time_start, time_end)
|
||||
|
||||
# return exit code 0 if processed events
|
||||
sys.exit(0 if added>0 else 1)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from . import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class EventClass(set, Enum):
|
||||
CGM_START_JOIN_STOP = {*_CGM_START, *_CGM_JOIN, *_CGM_STOP}
|
||||
CGM_READING = {events.LidCgmDataGxb, events.LidCgmDataG7, events.LidCgmDataFsl2}
|
||||
USER_MODE = {events.LidAaUserModeChange}
|
||||
DEVICE_STATUS = {events.LidDailyBasal}
|
||||
|
||||
@staticmethod
|
||||
def for_event(evt):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,22 @@ 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 todict(self):
|
||||
return dict(
|
||||
id=self.ID,
|
||||
name=self.NAME,
|
||||
seqNum=self.seqNum,
|
||||
eventTimestamp=str(self.eventTimestamp),
|
||||
{fields_dict}
|
||||
)
|
||||
|
||||
'''
|
||||
|
||||
def firstLower(text):
|
||||
@@ -85,13 +92,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']
|
||||
@@ -99,6 +106,14 @@ def build_fields(event_def):
|
||||
return '\n'.join([f'{" "*4}{f}' for f in ret])
|
||||
|
||||
|
||||
def build_fields_dict(event_def):
|
||||
ret = []
|
||||
for name, field in event_def["data"].items():
|
||||
suffix = 'Raw' if "transform" in field and name[-3:] != 'Raw' else ''
|
||||
f = f'{fieldNameFormat(name)}{suffix}=self.{fieldNameFormat(name)}{suffix},'
|
||||
ret.append(f)
|
||||
return '\n'.join([f'{" "*12}{f}' for f in ret])
|
||||
|
||||
|
||||
def build_decode(event_def):
|
||||
p1s = []
|
||||
@@ -107,7 +122,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)
|
||||
@@ -136,6 +151,7 @@ def build_event(event_id, event_def):
|
||||
return TEMPLATE.format(
|
||||
name = eventNameFormat(event_def["name"]),
|
||||
fields = build_fields(event_def),
|
||||
fields_dict = build_fields_dict(event_def),
|
||||
build_p1 = build_decode(event_def)[0],
|
||||
build_p2 = build_decode(event_def)[1],
|
||||
transform_funcs = build_transform_funcs(event_def),
|
||||
@@ -160,13 +176,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1114
-65
File diff suppressed because it is too large
Load Diff
@@ -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):
|
||||
|
||||
@@ -10,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
|
||||
@@ -39,3 +54,20 @@ class RawEvent:
|
||||
# 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
|
||||
|
||||
def todict(self):
|
||||
return dict(
|
||||
id=self.id,
|
||||
name="RawEvent",
|
||||
seqNum=self.seqNum,
|
||||
eventTimestamp=str(self.eventTimestamp),
|
||||
raw=''.join('{:02x}'.format(x) for x in self.raw),
|
||||
)
|
||||
|
||||
@@ -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-14)+self.batterychargepercentlsbRaw)/(3*256)',
|
||||
''
|
||||
]
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ class NightscoutApi:
|
||||
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
|
||||
}, verify=self.verify)
|
||||
if latest.status_code != 200:
|
||||
if 'as a valid ISO-8601 date' in latest.text:
|
||||
logger.warning("Nightscout last_uploaded_entry %s could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (eventType, time_start, time_end, dateFilter))
|
||||
return None
|
||||
raise ApiException(latest.status_code, "Nightscout last_uploaded_entry %s response: %s" % (latest.status_code, latest.text))
|
||||
|
||||
j = latest.json()
|
||||
@@ -106,6 +109,9 @@ class NightscoutApi:
|
||||
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
|
||||
}, verify=self.verify)
|
||||
if latest.status_code != 200:
|
||||
if 'as a valid ISO-8601 date' in latest.text:
|
||||
logger.warning("Nightscout last_uploaded_bg_entry could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (time_start, time_end, dateFilter))
|
||||
return None
|
||||
raise ApiException(latest.status_code, "Nightscout last_uploaded_bg_entry %s response: %s" % (latest.status_code, latest.text))
|
||||
|
||||
j = latest.json()
|
||||
@@ -134,6 +140,9 @@ class NightscoutApi:
|
||||
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
|
||||
}, verify=self.verify)
|
||||
if latest.status_code != 200:
|
||||
if 'as a valid ISO-8601 date' in latest.text:
|
||||
logger.warning("Nightscout activity %s could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (activityType, time_start, time_end, dateFilter))
|
||||
return None
|
||||
raise ApiException(latest.status_code, "Nightscout activity %s response: %s" % (latest.status_code, latest.text))
|
||||
|
||||
j = latest.json()
|
||||
@@ -154,6 +163,36 @@ 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:
|
||||
if 'as a valid ISO-8601 date' in latest.text:
|
||||
logger.warning("Nightscout devicestatus could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (time_start, time_end, dateFilter))
|
||||
return None
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -194,6 +194,22 @@ class NightscoutEntry:
|
||||
"pump_event_id": pump_event_id
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def devicestatus(created_at, batteryVoltage, batteryPercent, pump_event_id=""):
|
||||
return {
|
||||
"device": ENTERED_BY,
|
||||
"created_at": created_at,
|
||||
"pump": {
|
||||
"clock": created_at,
|
||||
"battery": {
|
||||
"voltage": float(batteryVoltage),
|
||||
"percent": int(batteryPercent) if batteryPercent else None,
|
||||
"status": "%.0f%s" % (batteryPercent, '%')
|
||||
},
|
||||
},
|
||||
"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 +267,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",
|
||||
|
||||
@@ -70,15 +70,19 @@ AUTOUPDATE_MAX_SLEEP_SECONDS = get_number('AUTOUPDATE_MAX_SLEEP_SECONDS', '1500'
|
||||
AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS = get_number('AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS', '60') # 1 minute
|
||||
AUTOUPDATE_USE_FIXED_SLEEP = get_bool('AUTOUPDATE_USE_FIXED_SLEEP', 'false')
|
||||
AUTOUPDATE_NO_DATA_FAILURE_MINUTES = get_number('AUTOUPDATE_NO_DATA_FAILURE_MINUTES', '180') # 3 hours
|
||||
AUTOUPDATE_FAILURE_MINUTES = get_number('AUTOUPDATE_FAILURE_MINUTES', '15') # 15 minutes
|
||||
AUTOUPDATE_RESTART_ON_FAILURE = get_bool('AUTOUPDATE_RESTART_ON_FAILURE', 'true')
|
||||
AUTOUPDATE_FAILURE_MINUTES = get_number('AUTOUPDATE_FAILURE_MINUTES', '75') # 75 minutes
|
||||
AUTOUPDATE_RESTART_ON_FAILURE = get_bool('AUTOUPDATE_RESTART_ON_FAILURE', 'false')
|
||||
AUTOUPDATE_MAX_LOOP_INVOCATIONS = get_number('AUTOUPDATE_MAX_LOOP_INVOCATIONS', '-1')
|
||||
|
||||
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')
|
||||
|
||||
@@ -18,7 +18,7 @@ class TandemSourceAutoupdate:
|
||||
self.last_max_date_with_events = None
|
||||
self.last_event_time = 0
|
||||
self.last_attempt_time = 0
|
||||
self.last_event_index = None
|
||||
self.last_event_seqnum = None
|
||||
self.time_diffs_between_attempts = []
|
||||
self.time_diffs_between_updates = []
|
||||
|
||||
@@ -42,7 +42,7 @@ class TandemSourceAutoupdate:
|
||||
|
||||
tconnectDevice = ChooseDevice(self.secret, tconnect).choose()
|
||||
|
||||
event_id = None
|
||||
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))
|
||||
@@ -50,26 +50,31 @@ class TandemSourceAutoupdate:
|
||||
if pretend:
|
||||
logger.info('Would update now if not in pretend mode')
|
||||
else:
|
||||
added, event_id = 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_id:
|
||||
self.last_event_index = event_id
|
||||
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_attempt_time = now
|
||||
self.time_diffs_between_attempts = []
|
||||
else:
|
||||
logger.info('No new reported tandemsource data. (cur_max_date: %s)' % cur_max_date_with_events)
|
||||
logger.info('No new reported tandemsource data. cur_max_date: %s (%dm ago) last_event_time: %s (%dm ago)' % (
|
||||
arrow.get(cur_max_date_with_events) if cur_max_date_with_events else None,
|
||||
(now - cur_max_date_with_events)//60 if cur_max_date_with_events else None,
|
||||
arrow.get(self.last_event_time) if self.last_event_time else None,
|
||||
(now - self.last_event_time)//60 if self.last_event_time else None
|
||||
))
|
||||
|
||||
# If we haven't seen the pump event index update in AUTOUPDATE_NO_DATA_FAILURE_MINUTES,
|
||||
# then trigger an error and potentially restart.
|
||||
@@ -93,7 +98,7 @@ class TandemSourceAutoupdate:
|
||||
# above no indexes warning.
|
||||
elif self.last_successful_process_time_range and (now - self.last_successful_process_time_range) >= 60 * self.secret.AUTOUPDATE_FAILURE_MINUTES:
|
||||
logger.error(AutoupdateNoNewDataDetectedError(
|
||||
"%s: No new data has been detected via the API for %d minutes. " % (datetime.datetime.now(), (now - self.last_successful_process_time_range)//60) +
|
||||
"%s: No new data has been detected via the API for %d minutes (last: %s). " % (datetime.datetime.now(), (now - self.last_successful_process_time_range)//60, self.last_successful_process_time_range) +
|
||||
"tconnectsync might not be functioning properly."))
|
||||
|
||||
if self.secret.AUTOUPDATE_RESTART_ON_FAILURE:
|
||||
|
||||
@@ -24,6 +24,20 @@ class ChooseDevice:
|
||||
|
||||
tconnectDevice = serialNumberToPump[str(self.secret.PUMP_SERIAL_NUMBER)]
|
||||
|
||||
# Warn if pump is stale (no events in >3 days)
|
||||
try:
|
||||
max_event_date = arrow.get(tconnectDevice["maxDateWithEvents"])
|
||||
age_days = (arrow.utcnow() - max_event_date).days
|
||||
|
||||
if age_days > 3:
|
||||
logger.warning(
|
||||
f"The selected pump (serial {tconnectDevice['serialNumber']}) has no events in the last {age_days} days "
|
||||
f"(last seen: {tconnectDevice['maxDateWithEvents']}). "
|
||||
"You may have switched to a new pump. Consider removing or updating PUMP_SERIAL_NUMBER in your config."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not parse maxDateWithEvents to check for staleness: {e}")
|
||||
|
||||
logger.info(f'Using pump with serial: {tconnectDevice["serialNumber"]} (tconnectDeviceId: {tconnectDevice["tconnectDeviceId"]}, last seen: {tconnectDevice["maxDateWithEvents"]})')
|
||||
else:
|
||||
maxDateSeen = None
|
||||
|
||||
@@ -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)
|
||||
@@ -14,4 +14,4 @@ def run_oneshot(tconnect, nightscout, pretend=False, features=DEFAULT_FEATURES,
|
||||
secret_arg = secret
|
||||
|
||||
tconnectDevice = ChooseDevice(secret_arg, tconnect).choose()
|
||||
return ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, features).process(time_start, time_end)
|
||||
return ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, secret_arg, features).process(time_start, time_end)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
import collections
|
||||
|
||||
from ...features import DEFAULT_FEATURES
|
||||
from ...eventparser.generic import Events, decode_raw_events, EVENT_LEN
|
||||
from ...features import DEVICE_STATUS, DEFAULT_FEATURES
|
||||
from ...eventparser import events as eventtypes
|
||||
from ...domain.tandemsource.event_class import EventClass
|
||||
from .process_basal import ProcessBasal
|
||||
@@ -14,18 +13,20 @@ from .process_cartridge import ProcessCartridge
|
||||
from .process_cgm_alert import ProcessCGMAlert
|
||||
from .process_cgm_start_join_stop import ProcessCGMStartJoinStop
|
||||
from .process_cgm_reading import ProcessCGMReading
|
||||
from .process_device_status import ProcessDeviceStatus
|
||||
from .process_user_mode import ProcessUserMode
|
||||
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 = {
|
||||
@@ -39,6 +40,7 @@ class ProcessTimeRange:
|
||||
EventClass.CGM_START_JOIN_STOP.name: ProcessCGMStartJoinStop,
|
||||
EventClass.CGM_READING.name: ProcessCGMReading,
|
||||
EventClass.USER_MODE.name: ProcessUserMode,
|
||||
EventClass.DEVICE_STATUS.name: ProcessDeviceStatus
|
||||
}
|
||||
|
||||
updater_classes = [
|
||||
@@ -46,27 +48,25 @@ 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}")
|
||||
fetch_all_event_types = self.secret.FETCH_ALL_EVENT_TYPES or DEVICE_STATUS in 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)
|
||||
logger.info(f"ProcessTimeRange time_start={time_start} time_end={time_end} tconnect_device_id={self.tconnect_device_id} features={self.features} fetch_all_event_types={fetch_all_event_types}")
|
||||
events = self.tconnect.tandemsource.pump_events(self.tconnect_device_id, time_start, time_end, fetch_all_event_types=fetch_all_event_types)
|
||||
|
||||
events_first_time = None
|
||||
events_last_time = None
|
||||
last_event_id = None # aka seqnum
|
||||
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_id:
|
||||
last_event_id = event.eventId
|
||||
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_id = max(event.eventId, last_event_id)
|
||||
last_event_seqnum = max(event.seqNum, last_event_seqnum)
|
||||
|
||||
clazz = EventClass.for_event(event)
|
||||
if clazz:
|
||||
@@ -82,7 +82,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))
|
||||
|
||||
@@ -95,6 +97,6 @@ class ProcessTimeRange:
|
||||
else:
|
||||
logger.info("Skipping %s, is not enabled from features %s" % (updater_class.__name__, self.features))
|
||||
|
||||
logger.info("Processed %d events. Last event ID seen: %d" % (processed_count, last_event_id))
|
||||
return processed_count, last_event_id
|
||||
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
|
||||
|
||||
|
||||
@@ -40,11 +40,20 @@ class ProcessAlarm:
|
||||
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))
|
||||
|
||||
|
||||
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
|
||||
@@ -54,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
|
||||
|
||||
@@ -73,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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ class ProcessBolus:
|
||||
if type(event) == eventtypes.LidBolusCompleted:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping bolusCompletedEvent before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,78 @@
|
||||
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 []
|
||||
|
||||
logger.info("ProcessDeviceStatus: last_daily_basal_event=%s" % (last_daily_basal_event))
|
||||
|
||||
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),
|
||||
batteryPercent=int(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
|
||||
@@ -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,7 +191,7 @@ 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"
|
||||
@@ -204,7 +204,7 @@ class ProcessUserMode:
|
||||
reason=reason + " - " + NOT_ENDED,
|
||||
duration=duration_mins,
|
||||
event_type=EXERCISE_EVENTTYPE,
|
||||
pump_event_id = "%s" % start.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
|
||||
|
||||
@@ -49,10 +49,12 @@ class UpdateProfiles:
|
||||
logger.info("Current pump settings: %s" % pump_settings)
|
||||
|
||||
ns_profile_obj = self.nightscout.current_profile()
|
||||
logger.info("Current Nightscout profile: %s" % ns_profile_obj)
|
||||
logger.debug("Current Nightscout profile: %s" % ns_profile_obj)
|
||||
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")
|
||||
@@ -93,7 +95,7 @@ class UpdateProfiles:
|
||||
|
||||
ns = ns_profile_obj.get('store', {})
|
||||
|
||||
logger.info("compare_profiles profile names: device: %s ns: %s", device.keys(), ns.keys())
|
||||
logger.debug("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:
|
||||
@@ -111,12 +113,12 @@ class UpdateProfiles:
|
||||
|
||||
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)
|
||||
#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)
|
||||
#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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,154 @@
|
||||
#!/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': {
|
||||
'clock': '2024-12-03 23:40:23-05:00',
|
||||
'battery': {
|
||||
'status': '32%',
|
||||
'percent': 32,
|
||||
'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': {
|
||||
'clock': '2024-12-04 02:30:23-05:00',
|
||||
'battery': {
|
||||
'status': '31%',
|
||||
'percent': 31,
|
||||
'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': {
|
||||
'clock': '2024-12-04 05:50:23-05:00',
|
||||
'battery': {
|
||||
'status': '30%',
|
||||
'percent': 30,
|
||||
'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()
|
||||
Reference in New Issue
Block a user