mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-29 12:34:14 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d4c63a4d | ||
|
|
81a62b8902 | ||
|
|
2f5775cbfb | ||
|
|
f13929a45a | ||
|
|
a18c2f3d17 | ||
|
|
1b400d3b22 | ||
|
|
d3816b18b2 | ||
|
|
4a6dd5f51d | ||
|
|
51abc15512 |
@@ -8,3 +8,4 @@ build
|
||||
*.egg-info
|
||||
.env
|
||||
tconnectsync-check-output.log
|
||||
ignore_*
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 0.7.2
|
||||
version = 0.8.0
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -105,6 +105,7 @@ class ControlIQApi:
|
||||
|
||||
"""
|
||||
Returns detailed basal event information and reasons for delivery suspension.
|
||||
End-date inclusive: Returns data from 00:00 on start date to 23:59 on end date.
|
||||
"""
|
||||
def therapy_timeline(self, start=None, end=None):
|
||||
startDate = parse_date(start)
|
||||
|
||||
@@ -71,6 +71,9 @@ class WS2Api:
|
||||
Basal data does NOT appear for the specified time range if using Control-IQ.
|
||||
The ControlIQ API endpoints must be used for basal data instead.
|
||||
However, all other fields are still accessed via this endpoint.
|
||||
|
||||
This has its own built-in retry logic because Tandem's frontend serving
|
||||
the API returns 500s when its backend times out.
|
||||
"""
|
||||
def therapy_timeline_csv(self, start=None, end=None, tries=0):
|
||||
startDate = parse_date(start)
|
||||
@@ -124,6 +127,8 @@ class WS2Api:
|
||||
- "manual"
|
||||
- "previous"
|
||||
- "alarm"
|
||||
|
||||
End-date inclusive: Returns data from 00:00 on start date to 23:59 on end date.
|
||||
|
||||
{"BasalSuspension":[{"EventDateTime":"/Date(EPOCH_MS-0000)/", "SuspendReason": "reason"}]}
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class Bolus:
|
||||
description: str
|
||||
complete: str # "1" / "0"
|
||||
completion: str
|
||||
request_time: str # _datetime_parse timestamp
|
||||
completion_time: str # _datetime_parse timestamp
|
||||
insulin: str
|
||||
requested_insulin: str
|
||||
carbs: str
|
||||
bg: str # potentially ""
|
||||
user_override: str
|
||||
extended_bolus: str # "1" / "0"
|
||||
bolex_completion_time: str
|
||||
bolex_start_time: str
|
||||
|
||||
def to_dict(self):
|
||||
return asdict(self)
|
||||
|
||||
@property
|
||||
def is_extended_bolus(self):
|
||||
return self.extended_bolus == "1"
|
||||
@@ -0,0 +1,367 @@
|
||||
import arrow
|
||||
|
||||
from tconnectsync.domain.bolus import Bolus
|
||||
|
||||
from ..secret import TIMEZONE_NAME
|
||||
|
||||
|
||||
def _datetime_parse(date):
|
||||
# consistent format with ws2 endpoint
|
||||
return arrow.get(date, tzinfo=TIMEZONE_NAME).format("YYYY-MM-DD HH:mm:ssZZ")
|
||||
|
||||
class TherapyEvent:
|
||||
type = None
|
||||
eventDateTime = None
|
||||
sourceRecId = None
|
||||
|
||||
def parse(self, json):
|
||||
self.type = json['type']
|
||||
self.eventDateTime = json['eventDateTime']
|
||||
self.sourceRecId = json['sourceRecId']
|
||||
self.rawJson = json
|
||||
|
||||
class CGMTherapyEvent(TherapyEvent):
|
||||
eventID = None
|
||||
egv = None
|
||||
"""
|
||||
{
|
||||
"eventDateTime": "2022-07-21T00:00:08",
|
||||
"eventID": 256,
|
||||
"requestDateTime": "0001-01-01T00:00:00",
|
||||
"type": "CGM",
|
||||
"description": "EGV",
|
||||
"sourceRecId": 0,
|
||||
"eventTypeId": 0,
|
||||
"deviceType": "t:slim X2 Insulin Pump",
|
||||
"serialNumber": "xxx",
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0,
|
||||
"egv": {
|
||||
"estimatedGlucoseValue": 174,
|
||||
"hypo": 0,
|
||||
"belowTarget": 0,
|
||||
"withinTarget": 1,
|
||||
"aboveTarget": 0,
|
||||
"hyper": 0
|
||||
}
|
||||
},
|
||||
"""
|
||||
@classmethod
|
||||
def parse(self, json):
|
||||
TherapyEvent.parse(self, json)
|
||||
self.eventID = json['eventID']
|
||||
self.egv = json['egv']['estimatedGlucoseValue']
|
||||
return self
|
||||
|
||||
class BolusTherapyEvent(TherapyEvent):
|
||||
bolusRequestOptions = None
|
||||
REQUEST_AUTOMATIC = "Automatic Bolus/Correction"
|
||||
REQUEST_STANDARD = "Standard"
|
||||
|
||||
bolusType = None
|
||||
TYPE_AUTOMATIC = "Automatic Correction"
|
||||
TYPE_CARB = "Carb"
|
||||
|
||||
carbSize = None
|
||||
correctionBolusSize = None
|
||||
foodBolusSize = None
|
||||
|
||||
insulinDelivered = None
|
||||
insulinRequested = None
|
||||
completionDateTime = None
|
||||
|
||||
completionStatus = None
|
||||
STATUS_COMPLETED = "Completed"
|
||||
|
||||
eventHistoryReportDetails = None
|
||||
|
||||
standardPercent = None
|
||||
sourceRecId = None
|
||||
|
||||
@classmethod
|
||||
def parse(_, json):
|
||||
self = BolusTherapyEvent()
|
||||
TherapyEvent.parse(self, json)
|
||||
self.description = json.get("description")
|
||||
self.complete = json.get("standard", {}).get("bolusIsComplete")
|
||||
self.completion = json.get("standard", {}).get("completionStatusDesc")
|
||||
self.request_time = json.get("requestDateTime")
|
||||
self.completion_time = json.get("standard", {}).get("insulinDelivered", {}).get("completionDateTime")
|
||||
# TODO: separate extended vs standard bolus into separate fields
|
||||
self.insulin = json.get("standard", {}).get("insulinDelivered", {}).get("value")
|
||||
self.requested_insulin = json.get("standard", {}).get("insulinRequested")
|
||||
self.carbs = json.get("carbSize")
|
||||
self.bg = json.get("bg")
|
||||
self.user_override = json.get("userOverride")
|
||||
self.extended_bolus = json.get("bolusRequestOptions") == "Extended"
|
||||
if self.extended_bolus and self.complete:
|
||||
# TODO(https://github.com/jwoglom/tconnectsync/issues/19): read more extended bolus info
|
||||
self.complete = json.get("bolex", {}).get("extendedBolusIsComplete")
|
||||
self.completion = json.get("bolex", {}).get("completionStatusDesc")
|
||||
self.bolex_completion_time = json.get("bolex", {}).get("insulinDelivered", {}).get("completionDateTime")
|
||||
self.bolex_start_time = json.get("bolex", {}).get("bolexStartDateTime")
|
||||
else:
|
||||
self.bolex_completion_time = ""
|
||||
self.bolex_start_time = ""
|
||||
return self
|
||||
|
||||
def to_bolus(self):
|
||||
return Bolus(
|
||||
description=self.description,
|
||||
complete="1" if self.complete else "0",
|
||||
completion=self.completion or "",
|
||||
request_time=_datetime_parse(self.request_time),
|
||||
completion_time=_datetime_parse(self.completion_time),
|
||||
insulin=str(self.insulin),
|
||||
requested_insulin=str(self.requested_insulin),
|
||||
carbs=str(self.carbs or "0"), # Nightscout expects non-empty carbs
|
||||
bg=str(self.bg or ""),
|
||||
user_override=str(self.user_override),
|
||||
extended_bolus="1" if self.extended_bolus else "0",
|
||||
bolex_completion_time=_datetime_parse(self.bolex_completion_time) if self.bolex_completion_time else "",
|
||||
bolex_start_time=_datetime_parse(self.bolex_start_time) if self.bolex_start_time else ""
|
||||
)
|
||||
|
||||
"""
|
||||
Correction:
|
||||
{
|
||||
"actualTotalBolusRequested": 2.9,
|
||||
"bg": 254,
|
||||
"bolusRequestOptions": "Automatic Bolus/Correction",
|
||||
"bolusType": "Automatic Correction",
|
||||
"carbSize": 0,
|
||||
"correctionBolusSize": 2.9,
|
||||
"correctionFactor": 30,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 0,
|
||||
"eventDateTime": "2022-07-21T11:53:08",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:0 - Target BG 110",
|
||||
"eventHistoryReportEventDesc": "Correction Bolus",
|
||||
"foodBolusSize": 0,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "572946",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": false
|
||||
},
|
||||
"requestDateTime": "2022-07-21T11:53:08",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-07-21T11:55:24",
|
||||
"value": 2.9
|
||||
},
|
||||
"foodDelivered": 0,
|
||||
"correctionDelivered": 2.9,
|
||||
"insulinRequested": 2.9,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3361,
|
||||
"bolusCompletionId": 3361
|
||||
},
|
||||
"standardPercent": 100,
|
||||
"targetBG": 110,
|
||||
"userOverride": 0,
|
||||
"type": "Bolus",
|
||||
"description": "Automatic Bolus/Correction",
|
||||
"sourceRecId": 1171791787,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
},
|
||||
Standard:
|
||||
{
|
||||
"actualTotalBolusRequested": 4.17,
|
||||
"bolusRequestOptions": "Standard",
|
||||
"bolusType": "Carb",
|
||||
"carbSize": 25,
|
||||
"correctionBolusSize": 0,
|
||||
"correctionFactor": 30,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 0,
|
||||
"eventDateTime": "2022-07-21T12:27:36",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:6 - Target BG 110",
|
||||
"eventHistoryReportEventDesc": "Food Bolus",
|
||||
"foodBolusSize": 4.17,
|
||||
"iob": 2.62,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "573042",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": false
|
||||
},
|
||||
"requestDateTime": "2022-07-21T12:27:36",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-07-21T12:29:21",
|
||||
"value": 4.17
|
||||
},
|
||||
"foodDelivered": 4.17,
|
||||
"correctionDelivered": 0,
|
||||
"insulinRequested": 4.17,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3362,
|
||||
"bolusCompletionId": 3362
|
||||
},
|
||||
"standardPercent": 100,
|
||||
"targetBG": 110,
|
||||
"userOverride": 0,
|
||||
"type": "Bolus",
|
||||
"description": "Standard",
|
||||
"sourceRecId": 1171853319,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
},
|
||||
Extended bolus incomplete:
|
||||
{
|
||||
"actualTotalBolusRequested": 0.4,
|
||||
"bg": 131,
|
||||
"bolex": {
|
||||
"size": 0.2,
|
||||
"bolexStartDateTime": "2022-08-09T23:20:04",
|
||||
"iob": 0,
|
||||
"completionStatusId": 0,
|
||||
"extendedBolusIsComplete": 0,
|
||||
"insulinRequested": 0,
|
||||
"bolexCompletionId": 0
|
||||
},
|
||||
"bolusRequestOptions": "Extended",
|
||||
"bolusType": "Carb",
|
||||
"carbSize": 0,
|
||||
"correctionBolusSize": 0.0,
|
||||
"correctionFactor": 30.0,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 15,
|
||||
"eventDateTime": "2022-08-09T23:19:15",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:6 - Target BG 110<br/>Override: Pump calculated Bolus = 0.0 units",
|
||||
"eventHistoryReportEventDesc": "Food Bolus: 50% Extended 15 mins",
|
||||
"foodBolusSize": 0.0,
|
||||
"iob": 5.87,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "631597",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": false
|
||||
},
|
||||
"requestDateTime": "2022-08-09T23:19:15",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-08-09T23:20:04",
|
||||
"value": 0.2
|
||||
},
|
||||
"foodDelivered": 0.0,
|
||||
"correctionDelivered": 0.0,
|
||||
"insulinRequested": 0.2,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3636.0,
|
||||
"bolusCompletionId": 3636.0
|
||||
},
|
||||
"standardPercent": 50.0,
|
||||
"targetBG": 110,
|
||||
"userOverride": 1,
|
||||
"type": "Bolus",
|
||||
"description": "Extended 50.00%/0.00",
|
||||
"sourceRecId": 1209631944,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
}
|
||||
Extended bolus (complete):
|
||||
{
|
||||
"actualTotalBolusRequested": 0.4,
|
||||
"bg": 131,
|
||||
"bolex": {
|
||||
"size": 0.2,
|
||||
"bolexStartDateTime": "2022-08-09T23:20:04",
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-08-09T23:35:03",
|
||||
"value": 0.2
|
||||
},
|
||||
"iob": 5.7,
|
||||
"completionStatusId": 3.0,
|
||||
"completionStatusDesc": "Completed",
|
||||
"extendedBolusIsComplete": 1,
|
||||
"insulinRequested": 0.2,
|
||||
"bolexCompletionId": 16757133
|
||||
},
|
||||
"bolusRequestOptions": "Extended",
|
||||
"bolusType": "Carb",
|
||||
"carbSize": 0,
|
||||
"correctionBolusSize": 0.0,
|
||||
"correctionFactor": 30.0,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 15,
|
||||
"eventDateTime": "2022-08-09T23:19:15",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:6 - Target BG 110<br/>Override: Pump calculated Bolus = 0.0 units",
|
||||
"eventHistoryReportEventDesc": "Food Bolus: 50% Extended 15 mins",
|
||||
"foodBolusSize": 0.0,
|
||||
"iob": 5.87,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "631597",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": false
|
||||
},
|
||||
"requestDateTime": "2022-08-09T23:19:15",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-08-09T23:20:04",
|
||||
"value": 0.2
|
||||
},
|
||||
"foodDelivered": 0.0,
|
||||
"correctionDelivered": 0.0,
|
||||
"insulinRequested": 0.2,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3636.0,
|
||||
"bolusCompletionId": 3636.0
|
||||
},
|
||||
"standardPercent": 50.0,
|
||||
"targetBG": 110,
|
||||
"userOverride": 1,
|
||||
"type": "Bolus",
|
||||
"description": "Extended 50.00%/0.00",
|
||||
"sourceRecId": 1209631944,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
}
|
||||
"""
|
||||
+22
-12
@@ -1,19 +1,26 @@
|
||||
import sys
|
||||
import datetime
|
||||
import requests
|
||||
import hashlib
|
||||
import time
|
||||
import urllib.parse
|
||||
import arrow
|
||||
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from .api.common import ApiException
|
||||
from .parser.nightscout import ENTERED_BY
|
||||
|
||||
# try:
|
||||
# from .secret import NS_URL, NS_SECRET
|
||||
# except Exception:
|
||||
# print('Unable to import Nightscout secrets from secret.py')
|
||||
# sys.exit(1)
|
||||
def format_datetime(date):
|
||||
return arrow.get(date).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def time_range(field_name, start_time, end_time):
|
||||
arg = ''
|
||||
if start_time:
|
||||
arg += '&find[%s][$gte]=%s' % (field_name, format_datetime(start_time))
|
||||
if end_time:
|
||||
arg += '&find[%s][$lte]=%s' % (field_name, format_datetime(end_time))
|
||||
return arg
|
||||
|
||||
|
||||
class NightscoutApi:
|
||||
def __init__(self, url, secret):
|
||||
@@ -48,8 +55,9 @@ class NightscoutApi:
|
||||
if r.status_code != 200:
|
||||
raise ApiException(r.status_code, "Nightscout put response: %s" % r.text)
|
||||
|
||||
def last_uploaded_entry(self, eventType):
|
||||
latest = requests.get(urljoin(self.url, 'api/v1/treatments?count=1&find[enteredBy]=' + urllib.parse.quote(ENTERED_BY) + '&find[eventType]=' + urllib.parse.quote(eventType) + '&ts=' + str(time.time())), headers={
|
||||
def last_uploaded_entry(self, eventType, time_start=None, time_end=None):
|
||||
dateFilter = time_range('created_at', time_start, time_end)
|
||||
latest = requests.get(urljoin(self.url, 'api/v1/treatments?count=1&find[enteredBy]=' + urllib.parse.quote(ENTERED_BY) + '&find[eventType]=' + urllib.parse.quote(eventType) + dateFilter + '&ts=' + str(time.time())), headers={
|
||||
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
|
||||
})
|
||||
if latest.status_code != 200:
|
||||
@@ -60,8 +68,9 @@ class NightscoutApi:
|
||||
return j[0]
|
||||
return None
|
||||
|
||||
def last_uploaded_bg_entry(self):
|
||||
latest = requests.get(urljoin(self.url, 'api/v1/entries.json?count=1&find[device]=' + urllib.parse.quote(ENTERED_BY) + '&ts=' + str(time.time())), headers={
|
||||
def last_uploaded_bg_entry(self, time_start=None, time_end=None):
|
||||
dateFilter = time_range('dateString', time_start, time_end)
|
||||
latest = requests.get(urljoin(self.url, 'api/v1/entries.json?count=1&find[device]=' + urllib.parse.quote(ENTERED_BY) + dateFilter + '&ts=' + str(time.time())), headers={
|
||||
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
|
||||
})
|
||||
if latest.status_code != 200:
|
||||
@@ -72,8 +81,9 @@ class NightscoutApi:
|
||||
return j[0]
|
||||
return None
|
||||
|
||||
def last_uploaded_activity(self, activityType):
|
||||
latest = requests.get(urljoin(self.url, 'api/v1/activity?find[enteredBy]=' + urllib.parse.quote(ENTERED_BY) + '&find[activityType]=' + urllib.parse.quote(activityType) + '&ts=' + str(time.time())), headers={
|
||||
def last_uploaded_activity(self, activityType, time_start=None, time_end=None):
|
||||
dateFilter = time_range('created_at', time_start, time_end)
|
||||
latest = requests.get(urljoin(self.url, 'api/v1/activity?find[enteredBy]=' + urllib.parse.quote(ENTERED_BY) + '&find[activityType]=' + urllib.parse.quote(activityType) + dateFilter + '&ts=' + str(time.time())), headers={
|
||||
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
|
||||
})
|
||||
if latest.status_code != 200:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent
|
||||
from tconnectsync.parser.tconnect import TConnectEntry
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def split_therapy_events(ciqTherapyEvents):
|
||||
bolusEvents = []
|
||||
cgmEvents = []
|
||||
for e in ciqTherapyEvents['event']:
|
||||
event = TConnectEntry.parse_therapy_event(e)
|
||||
if type(event) == BolusTherapyEvent:
|
||||
bolusEvents.append(event)
|
||||
elif type(event) == CGMTherapyEvent:
|
||||
cgmEvents.append(event)
|
||||
|
||||
logger.debug("split_therapy_events: %d bolus, %d CGM" % (len(bolusEvents), len(cgmEvents)))
|
||||
return bolusEvents, cgmEvents
|
||||
@@ -48,7 +48,7 @@ class NightscoutEntry:
|
||||
}
|
||||
if bg:
|
||||
if bg_type not in (NightscoutEntry.SENSOR, NightscoutEntry.FINGER):
|
||||
raise InvalidBolusTypeException
|
||||
raise InvalidBolusTypeException("bg_type: %s (%s)" % (bg_type, data))
|
||||
|
||||
data.update({
|
||||
"glucose": str(bg),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from os import stat
|
||||
import sys
|
||||
import arrow
|
||||
from tconnectsync.domain.bolus import Bolus
|
||||
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent
|
||||
|
||||
try:
|
||||
from ..secret import TIMEZONE_NAME
|
||||
@@ -105,7 +108,7 @@ class TConnectEntry:
|
||||
complete = is_complete(data["ExtendedBolusIsComplete"]) or is_complete(data["BolusIsComplete"])
|
||||
extended_bolus = ("extended" in data["Description"].lower())
|
||||
|
||||
return {
|
||||
return Bolus(**{
|
||||
"description": data["Description"],
|
||||
"complete": "1" if complete else "",
|
||||
"completion": data["CompletionStatusDesc"] if not extended_bolus else data["BolexCompletionStatusDesc"],
|
||||
@@ -120,7 +123,7 @@ class TConnectEntry:
|
||||
# Note: completion time can be empty if the extended bolus is in progress
|
||||
"bolex_completion_time": TConnectEntry._datetime_parse(data["BolexCompletionDateTime"]).format() if data["BolexCompletionDateTime"] and complete and extended_bolus else None,
|
||||
"bolex_start_time": TConnectEntry._datetime_parse(data["BolexStartDateTime"]).format() if data["BolexStartDateTime"] and complete and extended_bolus else None,
|
||||
}
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def parse_reading_entry(data):
|
||||
@@ -189,7 +192,16 @@ class TConnectEntry:
|
||||
"time": time.format(),
|
||||
"event_type": TConnectEntry.BASALSUSPENSION_EVENTS[data["SuspendReason"]]
|
||||
}
|
||||
|
||||
|
||||
# Parses an entry from controliq.therapy_events() and returns a TherapyEvent
|
||||
@staticmethod
|
||||
def parse_therapy_event(data):
|
||||
if data["type"] == "Bolus":
|
||||
return BolusTherapyEvent.parse(data)
|
||||
elif data["type"] == "CGM":
|
||||
return CGMTherapyEvent.parse(data)
|
||||
|
||||
raise UnknownTherapyEventException(data)
|
||||
|
||||
class UnknownCIQActivityEventException(Exception):
|
||||
def __init__(self, data):
|
||||
@@ -197,4 +209,8 @@ class UnknownCIQActivityEventException(Exception):
|
||||
|
||||
class UnknownBasalSuspensionEventException(Exception):
|
||||
def __init__(self, data):
|
||||
super().__init__("Unknown basal suspension event type: %s" % data)
|
||||
super().__init__("Unknown basal suspension event type: %s" % data)
|
||||
|
||||
class UnknownTherapyEventException(Exception):
|
||||
def __init__(self, data):
|
||||
super().__init__("Unknown therapy event type: " % data)
|
||||
|
||||
+90
-28
@@ -3,6 +3,8 @@ import datetime
|
||||
import arrow
|
||||
import time
|
||||
|
||||
from tconnectsync.parser.ciq_therapy_events import split_therapy_events
|
||||
|
||||
from .util import timeago
|
||||
from .api.common import ApiException
|
||||
from .sync.basal import (
|
||||
@@ -29,6 +31,7 @@ from .sync.pump_events import (
|
||||
)
|
||||
from .parser.tconnect import TConnectEntry
|
||||
from .features import BASAL, BOLUS, IOB, BOLUS_BG, CGM, DEFAULT_FEATURES, PUMP_EVENTS
|
||||
from tconnectsync.sync import basal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,33 +53,78 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
|
||||
ciqTherapyTimelineData = None
|
||||
else:
|
||||
raise e
|
||||
|
||||
csvReadingData = None
|
||||
csvIobData = None
|
||||
csvBasalData = None
|
||||
csvBolusData = None
|
||||
|
||||
logger.info("Downloading t:connect CSV data")
|
||||
csvdata = tconnect.ws2.therapy_timeline_csv(time_start, time_end)
|
||||
ciqBolusData = None
|
||||
ciqReadingData = None
|
||||
if BOLUS in features:
|
||||
logger.info("Downloading t:connect therapy_events")
|
||||
ciqTherapyEventsData = tconnect.controliq.therapy_events(time_start, time_end)
|
||||
ciqBolusData, ciqReadingData = split_therapy_events(ciqTherapyEventsData)
|
||||
|
||||
readingData = csvdata["readingData"]
|
||||
iobData = csvdata["iobData"]
|
||||
csvBasalData = csvdata["basalData"]
|
||||
bolusData = csvdata["bolusData"]
|
||||
if ciqReadingData and len(ciqReadingData) > 0:
|
||||
lastReading = ciqReadingData[-1].eventDateTime
|
||||
lastReading = TConnectEntry._datetime_parse(lastReading)
|
||||
logger.debug(ciqReadingData[-1])
|
||||
logger.info("Last CGM reading from t:connect CIQ: %s (%s)" % (lastReading, timeago(lastReading)))
|
||||
else:
|
||||
logger.warning("No last CGM reading is able to be determined from CIQ")
|
||||
|
||||
if ciqBolusData and len(ciqBolusData) > 0:
|
||||
lastBolus = ciqBolusData[-1].eventDateTime
|
||||
lastReading = TConnectEntry._datetime_parse(lastBolus)
|
||||
logger.debug(ciqBolusData[-1].to_bolus())
|
||||
logger.info("Last bolus from t:connect CIQ: %s (%s)" % (lastBolus, timeago(lastBolus)))
|
||||
|
||||
bolusFallingBack = (BOLUS in features and not ciqBolusData)
|
||||
ciqFallingBack = (CGM in features and not ciqReadingData)
|
||||
if bolusFallingBack or \
|
||||
ciqFallingBack or \
|
||||
BOLUS_BG in features or \
|
||||
IOB in features:
|
||||
logger.warn("Downloading t:connect CSV data")
|
||||
logger.warn("<!!> This data source is unreliable and may prevent timely synchronization")
|
||||
if bolusFallingBack:
|
||||
logger.warn("Falling back on WS2 data source because BOLUS is an enabled feature and CIQ bolus data was empty!!")
|
||||
if ciqFallingBack:
|
||||
logger.warn("Falling back on WS2 data source because CGM is an enabled feature and CIQ cgm data was empty!!")
|
||||
if BOLUS_BG in features:
|
||||
logger.warn("<!!> Falling back on WS2 data source because BOLUS_BG is an enabled feature. " +
|
||||
"Please consider disabling this feature to improve synchronization reliability.")
|
||||
if IOB in features:
|
||||
logger.warn("<!!> Falling back on WS2 data source because IOB is an enabled feature. " +
|
||||
"Please consider disabling this feature to improve synchronization reliability.")
|
||||
csvdata = tconnect.ws2.therapy_timeline_csv(time_start, time_end)
|
||||
|
||||
csvReadingData = csvdata["readingData"]
|
||||
csvIobData = csvdata["iobData"]
|
||||
csvBasalData = csvdata["basalData"]
|
||||
csvBolusData = csvdata["bolusData"]
|
||||
|
||||
if csvReadingData and len(csvReadingData) > 0:
|
||||
lastReading = csvReadingData[-1]['EventDateTime'] if 'EventDateTime' in csvReadingData[-1] else 0
|
||||
lastReading = TConnectEntry._datetime_parse(lastReading)
|
||||
logger.debug(csvReadingData[-1])
|
||||
logger.info("Last CGM reading from t:connect CSV: %s (%s)" % (lastReading, timeago(lastReading)))
|
||||
else:
|
||||
logger.warning("No last CGM reading is able to be determined from CSV")
|
||||
|
||||
if readingData and len(readingData) > 0:
|
||||
lastReading = readingData[-1]['EventDateTime'] if 'EventDateTime' in readingData[-1] else 0
|
||||
lastReading = TConnectEntry._datetime_parse(lastReading)
|
||||
logger.debug(readingData[-1])
|
||||
logger.info("Last CGM reading from t:connect: %s (%s)" % (lastReading, timeago(lastReading)))
|
||||
else:
|
||||
logger.warning("No last CGM reading is able to be determined")
|
||||
|
||||
added = 0
|
||||
|
||||
cgmData = None
|
||||
if CGM in features or BOLUS_BG in features:
|
||||
logger.debug("Processing CGM events")
|
||||
cgmData = process_cgm_events(readingData)
|
||||
|
||||
if CGM in features:
|
||||
logger.debug("Writing CGM events")
|
||||
added += ns_write_cgm_events(nightscout, cgmData, pretend)
|
||||
if csvReadingData:
|
||||
cgmData = None
|
||||
if CGM in features or BOLUS_BG in features:
|
||||
logger.debug("Processing CGM events")
|
||||
cgmData = process_cgm_events(csvReadingData)
|
||||
|
||||
if CGM in features:
|
||||
logger.debug("Writing CGM events")
|
||||
added += ns_write_cgm_events(nightscout, cgmData, pretend, time_start=time_start, time_end=time_end)
|
||||
|
||||
if BASAL in features:
|
||||
basalEvents = process_ciq_basal_events(ciqTherapyTimelineData)
|
||||
@@ -85,8 +133,11 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
|
||||
add_csv_basal_events(basalEvents, csvBasalData)
|
||||
else:
|
||||
logger.debug("No CSV basal data found")
|
||||
|
||||
if basalEvents and len(basalEvents) > 0:
|
||||
logger.info("Last basal event from CIQ: %s" % basalEvents[-1])
|
||||
|
||||
added += ns_write_basal_events(nightscout, basalEvents, pretend=pretend)
|
||||
added += ns_write_basal_events(nightscout, basalEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
|
||||
if PUMP_EVENTS in features:
|
||||
pumpEvents = process_ciq_activity_events(ciqTherapyTimelineData)
|
||||
@@ -99,15 +150,26 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
|
||||
|
||||
pumpEvents += bsPumpEvents
|
||||
|
||||
added += ns_write_pump_events(nightscout, pumpEvents, pretend=pretend)
|
||||
added += ns_write_pump_events(nightscout, pumpEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
|
||||
if BOLUS in features:
|
||||
bolusEvents = process_bolus_events(bolusData)
|
||||
added += ns_write_bolus_events(nightscout, bolusEvents, pretend=pretend, include_bg=(BOLUS_BG in features))
|
||||
bolusEvents = []
|
||||
if ciqBolusData:
|
||||
logger.info("Processing ciqBolusData (%d entries)" % len(ciqBolusData))
|
||||
bolusEvents = process_bolus_events(ciqBolusData, source="ciq")
|
||||
|
||||
if IOB in features:
|
||||
iobEvents = process_iob_events(iobData)
|
||||
added += ns_write_iob_events(nightscout, iobEvents, pretend=pretend)
|
||||
if csvBolusData and not bolusEvents:
|
||||
logger.warn("Falling back on non-CIQ csvBolusData")
|
||||
bolusEvents = process_bolus_events(csvBolusData, source="csv")
|
||||
logger.debug("ciq bolusEvents: %s" % bolusEvents)
|
||||
|
||||
logger.info("finalized bolusEvents: %s" % bolusEvents)
|
||||
added += ns_write_bolus_events(nightscout, bolusEvents, pretend=pretend, include_bg=(BOLUS_BG in features), time_start=time_start, time_end=time_end)
|
||||
|
||||
if csvIobData:
|
||||
if IOB in features:
|
||||
iobEvents = process_iob_events(csvIobData)
|
||||
added += ns_write_iob_events(nightscout, iobEvents, pretend=pretend)
|
||||
|
||||
logger.info("Wrote %d events to Nightscout this process cycle" % added)
|
||||
return added
|
||||
@@ -104,9 +104,9 @@ def add_csv_basal_events(basalEvents, data):
|
||||
"""
|
||||
Given processed basal data, adds basal events to Nightscout.
|
||||
"""
|
||||
def ns_write_basal_events(nightscout, basalEvents, pretend=False):
|
||||
def ns_write_basal_events(nightscout, basalEvents, pretend=False, time_start=None, time_end=None):
|
||||
logger.debug("ns_write_basal_events: querying for last uploaded entry")
|
||||
last_upload = nightscout.last_uploaded_entry(BASAL_EVENTTYPE)
|
||||
last_upload = nightscout.last_uploaded_entry(BASAL_EVENTTYPE, time_start=time_start, time_end=time_end)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
@@ -121,7 +121,7 @@ def ns_write_basal_events(nightscout, basalEvents, pretend=False):
|
||||
for event in basalEvents:
|
||||
if last_upload_time and arrow.get(event["time"]) < last_upload_time:
|
||||
if pretend:
|
||||
logger.info("Skipping basal event before last upload time: %s" % event)
|
||||
logger.info("Skipping basal event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
recent_needs_update = False
|
||||
|
||||
+30
-23
@@ -1,5 +1,6 @@
|
||||
import arrow
|
||||
import logging
|
||||
from tconnectsync.domain.bolus import Bolus
|
||||
|
||||
from tconnectsync.sync.cgm import find_event_at
|
||||
|
||||
@@ -15,26 +16,32 @@ logger = logging.getLogger(__name__)
|
||||
"""
|
||||
Given bolus data input from the therapy timeline CSV, converts it into a digestable format.
|
||||
"""
|
||||
def process_bolus_events(bolusdata, cgmEvents=None):
|
||||
def process_bolus_events(bolusdata, cgmEvents=None, source=""):
|
||||
bolusEvents = []
|
||||
|
||||
for b in bolusdata:
|
||||
parsed = TConnectEntry.parse_bolus_entry(b)
|
||||
if parsed["completion"] != "Completed":
|
||||
if parsed["insulin"] and float(parsed["insulin"]) > 0:
|
||||
parsed = None
|
||||
if source == "ciq":
|
||||
parsed = b.to_bolus()
|
||||
else:
|
||||
parsed = TConnectEntry.parse_bolus_entry(b)
|
||||
|
||||
assert type(parsed) == Bolus
|
||||
if parsed.completion != "Completed":
|
||||
if parsed.insulin and float(parsed.insulin) > 0:
|
||||
# Count non-completed bolus if any insulin was delivered (vs. the amount of insulin requested)
|
||||
parsed["description"] += " (%s: requested %s units)" % (parsed["completion"], parsed["requested_insulin"])
|
||||
parsed.description += " (%s: requested %s units)" % (parsed.completion, parsed.requested_insulin)
|
||||
else:
|
||||
logger.warning("Skipping non-completed bolus data (was a bolus in progress?): %s parsed: %s" % (b, parsed))
|
||||
logger.warning("Skipping non-completed %s bolus data (was a bolus in progress?): %s parsed: %s" % (source, b, parsed))
|
||||
continue
|
||||
|
||||
if parsed["bg"] and cgmEvents:
|
||||
requested_at = parsed["request_time"] if not parsed["extended_bolus"] else parsed["bolex_start_time"]
|
||||
parsed["bg_type"] = guess_bolus_bg_type(parsed["bg"], requested_at, cgmEvents)
|
||||
if parsed.bg and cgmEvents:
|
||||
requested_at = parsed.request_time if not parsed.extended_bolus else parsed.bolex_start_time
|
||||
parsed.bg_type = guess_bolus_bg_type(parsed.bg, requested_at, cgmEvents)
|
||||
|
||||
bolusEvents.append(parsed)
|
||||
|
||||
bolusEvents.sort(key=lambda event: arrow.get(event["request_time"] if not event["extended_bolus"] else event["bolex_start_time"]))
|
||||
bolusEvents.sort(key=lambda event: arrow.get(event.request_time if not event.is_extended_bolus else event.bolex_start_time))
|
||||
|
||||
return bolusEvents
|
||||
|
||||
@@ -57,9 +64,9 @@ def guess_bolus_bg_type(bg, created_at, cgmEvents):
|
||||
"""
|
||||
Given processed bolus data, adds bolus events to Nightscout.
|
||||
"""
|
||||
def ns_write_bolus_events(nightscout, bolusEvents, pretend=False, include_bg=False, reading_events=None):
|
||||
def ns_write_bolus_events(nightscout, bolusEvents, pretend=False, include_bg=False, reading_events=None, time_start=None, time_end=None):
|
||||
logger.debug("ns_write_bolus_events: querying for last uploaded entry")
|
||||
last_upload = nightscout.last_uploaded_entry(BOLUS_EVENTTYPE)
|
||||
last_upload = nightscout.last_uploaded_entry(BOLUS_EVENTTYPE, time_start=time_start, time_end=time_end)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
@@ -72,27 +79,27 @@ def ns_write_bolus_events(nightscout, bolusEvents, pretend=False, include_bg=Fal
|
||||
|
||||
add_count = 0
|
||||
for event in bolusEvents:
|
||||
created_at = event["completion_time"] if not event["extended_bolus"] else event["bolex_start_time"]
|
||||
created_at = event.completion_time if not event.is_extended_bolus else event.bolex_start_time
|
||||
if last_upload_time and arrow.get(created_at) <= last_upload_time:
|
||||
if pretend:
|
||||
logger.info("Skipping basal event before last upload time: %s" % event)
|
||||
logger.info("Skipping basal event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
if include_bg and event["bg"]:
|
||||
if include_bg and event.bg:
|
||||
entry = NightscoutEntry.bolus(
|
||||
bolus=event["insulin"],
|
||||
carbs=event["carbs"],
|
||||
bolus=event.insulin,
|
||||
carbs=event.carbs,
|
||||
created_at=created_at,
|
||||
notes="{}{}{}".format(event["description"], " (Override)" if event["user_override"] == "1" else "", " (Extended)" if event["extended_bolus"] == "1" else ""),
|
||||
bg=event["bg"],
|
||||
bg_type=event["bg_type"]
|
||||
notes="{}{}{}".format(event.description, " (Override)" if event.user_override == "1" else "", " (Extended)" if event.extended_bolus == "1" else ""),
|
||||
bg=event.bg,
|
||||
bg_type=event.bg_type
|
||||
)
|
||||
else:
|
||||
entry = NightscoutEntry.bolus(
|
||||
bolus=event["insulin"],
|
||||
carbs=event["carbs"],
|
||||
bolus=event.insulin,
|
||||
carbs=event.carbs,
|
||||
created_at=created_at,
|
||||
notes="{}{}{}".format(event["description"], " (Override)" if event["user_override"] == "1" else "", " (Extended)" if event["extended_bolus"] == "1" else "")
|
||||
notes="{}{}{}".format(event.description, " (Override)" if event.user_override == "1" else "", " (Extended)" if event.extended_bolus == "1" else "")
|
||||
)
|
||||
|
||||
add_count += 1
|
||||
|
||||
@@ -39,9 +39,9 @@ def find_event_at(cgmEvents, find_time):
|
||||
"""
|
||||
Given processed CGM data, adds reading entries to Nightscout.
|
||||
"""
|
||||
def ns_write_cgm_events(nightscout, cgmEvents, pretend=False):
|
||||
def ns_write_cgm_events(nightscout, cgmEvents, pretend=False, time_start=None, time_end=None):
|
||||
logger.debug("ns_write_cgm_events: querying for last uploaded entry")
|
||||
last_upload = nightscout.last_uploaded_bg_entry()
|
||||
last_upload = nightscout.last_uploaded_bg_entry(time_start=time_start, time_end=time_end)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["dateString"])
|
||||
@@ -52,7 +52,7 @@ def ns_write_cgm_events(nightscout, cgmEvents, pretend=False):
|
||||
created_at = event["time"]
|
||||
if last_upload_time and arrow.get(created_at) <= last_upload_time:
|
||||
if pretend:
|
||||
logger.info("Skipping CGM event before last upload time: %s" % event)
|
||||
logger.info("Skipping CGM event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
entry = NightscoutEntry.entry(
|
||||
|
||||
@@ -24,9 +24,9 @@ def process_iob_events(iobdata):
|
||||
"""
|
||||
Given processed IOB data, creates a single Nightscout activity definition to store IOB.
|
||||
"""
|
||||
def ns_write_iob_events(nightscout, iobEvents, pretend=False):
|
||||
def ns_write_iob_events(nightscout, iobEvents, pretend=False, time_start=None, time_end=None):
|
||||
logger.debug("ns_write_iob_events: querying for last uploaded entry")
|
||||
last_upload = nightscout.last_uploaded_activity(IOB_ACTIVITYTYPE)
|
||||
last_upload = nightscout.last_uploaded_activity(IOB_ACTIVITYTYPE, time_start=time_start, time_end=time_end)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
|
||||
@@ -49,7 +49,7 @@ def process_basalsuspension_events(data):
|
||||
"""
|
||||
Given processed pump event data (of various types), write them to Nightscout
|
||||
"""
|
||||
def ns_write_pump_events(nightscout, pumpEvents, pretend=False):
|
||||
def ns_write_pump_events(nightscout, pumpEvents, pretend=False, time_start=None, time_end=None):
|
||||
count = 0
|
||||
|
||||
siteChangeEvents = []
|
||||
@@ -80,17 +80,17 @@ def ns_write_pump_events(nightscout, pumpEvents, pretend=False):
|
||||
logger.debug("sleepEvents: %s" % sleepEvents)
|
||||
logger.debug("activityEvents: %s" % activityEvents)
|
||||
|
||||
count += ns_write_pump_sitechange_events(nightscout, siteChangeEvents, pretend=pretend)
|
||||
count += ns_write_empty_cart_events(nightscout, emptyCartEvents, pretend=pretend)
|
||||
count += ns_write_user_suspended_events(nightscout, userSuspendedEvents, pretend=pretend)
|
||||
count += ns_write_pump_sitechange_events(nightscout, siteChangeEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
count += ns_write_empty_cart_events(nightscout, emptyCartEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
count += ns_write_user_suspended_events(nightscout, userSuspendedEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
|
||||
count += ns_write_exercise_activity_events(nightscout, exerciseEvents, pretend=pretend)
|
||||
count += ns_write_sleep_activity_events(nightscout, sleepEvents, pretend=pretend)
|
||||
count += ns_write_activity_events(nightscout, activityEvents, pretend=pretend)
|
||||
count += ns_write_exercise_activity_events(nightscout, exerciseEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
count += ns_write_sleep_activity_events(nightscout, sleepEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
count += ns_write_activity_events(nightscout, activityEvents, pretend=pretend, time_start=time_start, time_end=time_end)
|
||||
|
||||
return count
|
||||
|
||||
def ns_write_pump_sitechange_events(nightscout, siteChangeEvents, pretend=False):
|
||||
def ns_write_pump_sitechange_events(nightscout, siteChangeEvents, pretend=False, time_start=None, time_end=None):
|
||||
return _ns_write_pump_events(
|
||||
nightscout,
|
||||
siteChangeEvents,
|
||||
@@ -99,9 +99,11 @@ def ns_write_pump_sitechange_events(nightscout, siteChangeEvents, pretend=False)
|
||||
reason=event["event_type"]
|
||||
),
|
||||
SITECHANGE_EVENTTYPE,
|
||||
pretend=pretend)
|
||||
pretend=pretend,
|
||||
time_start=time_start,
|
||||
time_end=time_end)
|
||||
|
||||
def ns_write_empty_cart_events(nightscout, emptyCartEvents, pretend=False):
|
||||
def ns_write_empty_cart_events(nightscout, emptyCartEvents, pretend=False, time_start=None, time_end=None):
|
||||
return _ns_write_pump_events(
|
||||
nightscout,
|
||||
emptyCartEvents,
|
||||
@@ -110,9 +112,11 @@ def ns_write_empty_cart_events(nightscout, emptyCartEvents, pretend=False):
|
||||
reason=event["event_type"]
|
||||
),
|
||||
BASALSUSPENSION_EVENTTYPE,
|
||||
pretend=pretend)
|
||||
pretend=pretend,
|
||||
time_start=time_start,
|
||||
time_end=time_end)
|
||||
|
||||
def ns_write_user_suspended_events(nightscout, userSuspendedEvents, pretend=False):
|
||||
def ns_write_user_suspended_events(nightscout, userSuspendedEvents, pretend=False, time_start=None, time_end=None):
|
||||
return _ns_write_pump_events(
|
||||
nightscout,
|
||||
userSuspendedEvents,
|
||||
@@ -121,9 +125,11 @@ def ns_write_user_suspended_events(nightscout, userSuspendedEvents, pretend=Fals
|
||||
reason=event["event_type"]
|
||||
),
|
||||
BASALSUSPENSION_EVENTTYPE,
|
||||
pretend=pretend)
|
||||
pretend=pretend,
|
||||
time_start=time_start,
|
||||
time_end=time_end)
|
||||
|
||||
def ns_write_exercise_activity_events(nightscout, exerciseEvents, pretend=False):
|
||||
def ns_write_exercise_activity_events(nightscout, exerciseEvents, pretend=False, time_start=None, time_end=None):
|
||||
return _ns_write_pump_events(
|
||||
nightscout,
|
||||
exerciseEvents,
|
||||
@@ -134,9 +140,11 @@ def ns_write_exercise_activity_events(nightscout, exerciseEvents, pretend=False)
|
||||
event_type=EXERCISE_EVENTTYPE
|
||||
),
|
||||
EXERCISE_EVENTTYPE,
|
||||
pretend=pretend)
|
||||
pretend=pretend,
|
||||
time_start=time_start,
|
||||
time_end=time_end)
|
||||
|
||||
def ns_write_sleep_activity_events(nightscout, sleepEvents, pretend=False):
|
||||
def ns_write_sleep_activity_events(nightscout, sleepEvents, pretend=False, time_start=None, time_end=None):
|
||||
return _ns_write_pump_events(
|
||||
nightscout,
|
||||
sleepEvents,
|
||||
@@ -147,9 +155,11 @@ def ns_write_sleep_activity_events(nightscout, sleepEvents, pretend=False):
|
||||
event_type=SLEEP_EVENTTYPE
|
||||
),
|
||||
SLEEP_EVENTTYPE,
|
||||
pretend=pretend)
|
||||
pretend=pretend,
|
||||
time_start=time_start,
|
||||
time_end=time_end)
|
||||
|
||||
def ns_write_activity_events(nightscout, activityEvents, pretend=False):
|
||||
def ns_write_activity_events(nightscout, activityEvents, pretend=False, time_start=None, time_end=None):
|
||||
return _ns_write_pump_events(
|
||||
nightscout,
|
||||
activityEvents,
|
||||
@@ -159,15 +169,17 @@ def ns_write_activity_events(nightscout, activityEvents, pretend=False):
|
||||
duration=event["duration_mins"]
|
||||
),
|
||||
ACTIVITY_EVENTTYPE,
|
||||
pretend=pretend)
|
||||
pretend=pretend,
|
||||
time_start=time_start,
|
||||
time_end=time_end)
|
||||
|
||||
def _ns_write_pump_events(nightscout, events, buildNsEventFunc, eventType, pretend=False):
|
||||
def _ns_write_pump_events(nightscout, events, buildNsEventFunc, eventType, pretend=False, time_start=None, time_end=None):
|
||||
if len(events) == 0:
|
||||
logger.debug("No %s events to process" % eventType)
|
||||
return 0
|
||||
|
||||
logger.debug("ns_write_pump_events: querying for last %s" % eventType)
|
||||
last_upload = nightscout.last_uploaded_entry(eventType)
|
||||
last_upload = nightscout.last_uploaded_entry(eventType, time_start=time_start, time_end=time_end)
|
||||
last_upload_time = None
|
||||
if last_upload:
|
||||
last_upload_time = arrow.get(last_upload["created_at"])
|
||||
@@ -192,7 +204,7 @@ def _ns_write_pump_events(nightscout, events, buildNsEventFunc, eventType, prete
|
||||
|
||||
if skip:
|
||||
if pretend:
|
||||
logger.info("Skipping %s event before last upload time: %s" % (eventType, event))
|
||||
logger.info("Skipping %s pump event before last upload time: %s (time range: %s - %s)" % (eventType, event, time_start, time_end))
|
||||
continue
|
||||
|
||||
entry = buildNsEventFunc(event)
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import dataclasses
|
||||
import unittest
|
||||
from tconnectsync.domain.bolus import Bolus
|
||||
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent
|
||||
|
||||
class TestCGMTherapyEvent(unittest.TestCase):
|
||||
maxDiff = None
|
||||
sampleJson = {
|
||||
"eventDateTime": "2022-07-21T00:00:08",
|
||||
"eventID": 256,
|
||||
"requestDateTime": "0001-01-01T00:00:00",
|
||||
"type": "CGM",
|
||||
"description": "EGV",
|
||||
"sourceRecId": 0,
|
||||
"eventTypeId": 0,
|
||||
"deviceType": "t:slim X2 Insulin Pump",
|
||||
"serialNumber": "xxx",
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0,
|
||||
"egv": {
|
||||
"estimatedGlucoseValue": 174,
|
||||
"hypo": 0,
|
||||
"belowTarget": 0,
|
||||
"withinTarget": 1,
|
||||
"aboveTarget": 0,
|
||||
"hyper": 0
|
||||
}
|
||||
}
|
||||
|
||||
def test_parse_cgm(self):
|
||||
e = CGMTherapyEvent.parse(self.sampleJson)
|
||||
self.assertEqual(e.type, "CGM")
|
||||
self.assertEqual(e.eventDateTime, "2022-07-21T00:00:08")
|
||||
self.assertEqual(e.sourceRecId, 0)
|
||||
self.assertEqual(e.eventID, 256)
|
||||
self.assertEqual(e.egv, 174)
|
||||
|
||||
class TestBolusTherapyEvent(unittest.TestCase):
|
||||
maxDiff = None
|
||||
standardJson = {
|
||||
"actualTotalBolusRequested": 4.17,
|
||||
"bolusRequestOptions": "Standard",
|
||||
"bolusType": "Carb",
|
||||
"carbSize": 25,
|
||||
"correctionBolusSize": 0,
|
||||
"correctionFactor": 30,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 0,
|
||||
"eventDateTime": "2022-07-21T12:27:36",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:6 - Target BG 110",
|
||||
"eventHistoryReportEventDesc": "Food Bolus",
|
||||
"foodBolusSize": 4.17,
|
||||
"iob": 2.62,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "573042",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": False
|
||||
},
|
||||
"requestDateTime": "2022-07-21T12:27:36",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-07-21T12:29:21",
|
||||
"value": 4.17
|
||||
},
|
||||
"foodDelivered": 4.17,
|
||||
"correctionDelivered": 0,
|
||||
"insulinRequested": 4.17,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3362,
|
||||
"bolusCompletionId": 3362
|
||||
},
|
||||
"standardPercent": 100,
|
||||
"targetBG": 110,
|
||||
"userOverride": 0,
|
||||
"type": "Bolus",
|
||||
"description": "Standard",
|
||||
"sourceRecId": 1171853319,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
}
|
||||
|
||||
def test_standard_to_bolus(self):
|
||||
e = BolusTherapyEvent.parse(self.standardJson)
|
||||
self.assertIsNotNone(e)
|
||||
b = e.to_bolus()
|
||||
self.assertEqual(dataclasses.asdict(b), dataclasses.asdict(Bolus(
|
||||
description="Standard",
|
||||
complete="1",
|
||||
completion="Completed",
|
||||
request_time="2022-07-21 12:27:36-04:00",
|
||||
completion_time="2022-07-21 12:29:21-04:00",
|
||||
insulin="4.17",
|
||||
requested_insulin="4.17",
|
||||
carbs="25",
|
||||
bg="",
|
||||
user_override="0",
|
||||
extended_bolus="0",
|
||||
bolex_completion_time="",
|
||||
bolex_start_time=""
|
||||
)))
|
||||
|
||||
correctionJson = {
|
||||
"actualTotalBolusRequested": 2.9,
|
||||
"bg": 254,
|
||||
"bolusRequestOptions": "Automatic Bolus/Correction",
|
||||
"bolusType": "Automatic Correction",
|
||||
"carbSize": 0,
|
||||
"correctionBolusSize": 2.9,
|
||||
"correctionFactor": 30,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 0,
|
||||
"eventDateTime": "2022-07-21T11:53:08",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:0 - Target BG 110",
|
||||
"eventHistoryReportEventDesc": "Correction Bolus",
|
||||
"foodBolusSize": 0,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "572946",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": False
|
||||
},
|
||||
"requestDateTime": "2022-07-21T11:53:08",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-07-21T11:55:24",
|
||||
"value": 2.9
|
||||
},
|
||||
"foodDelivered": 0,
|
||||
"correctionDelivered": 2.9,
|
||||
"insulinRequested": 2.9,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3361,
|
||||
"bolusCompletionId": 3361
|
||||
},
|
||||
"standardPercent": 100,
|
||||
"targetBG": 110,
|
||||
"userOverride": 0,
|
||||
"type": "Bolus",
|
||||
"description": "Automatic Bolus/Correction",
|
||||
"sourceRecId": 1171791787,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
}
|
||||
|
||||
def test_correction_to_bolus(self):
|
||||
e = BolusTherapyEvent.parse(self.correctionJson)
|
||||
self.assertIsNotNone(e)
|
||||
b = e.to_bolus()
|
||||
self.assertEqual(dataclasses.asdict(b), dataclasses.asdict(Bolus(
|
||||
description="Automatic Bolus/Correction",
|
||||
complete="1",
|
||||
completion="Completed",
|
||||
request_time="2022-07-21 11:53:08-04:00",
|
||||
completion_time="2022-07-21 11:55:24-04:00",
|
||||
insulin="2.9",
|
||||
requested_insulin="2.9",
|
||||
carbs="0",
|
||||
bg="254",
|
||||
user_override="0",
|
||||
extended_bolus="0",
|
||||
bolex_completion_time="",
|
||||
bolex_start_time=""
|
||||
)))
|
||||
|
||||
extendedBolusIncompleteJson = {
|
||||
"actualTotalBolusRequested": 0.4,
|
||||
"bg": 131,
|
||||
"bolex": {
|
||||
"size": 0.2,
|
||||
"bolexStartDateTime": "2022-08-09T23:20:04",
|
||||
"iob": 0,
|
||||
"completionStatusId": 0,
|
||||
"extendedBolusIsComplete": 0,
|
||||
"insulinRequested": 0,
|
||||
"bolexCompletionId": 0
|
||||
},
|
||||
"bolusRequestOptions": "Extended",
|
||||
"bolusType": "Carb",
|
||||
"carbSize": 0,
|
||||
"correctionBolusSize": 0.0,
|
||||
"correctionFactor": 30.0,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 15,
|
||||
"eventDateTime": "2022-08-09T23:19:15",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:6 - Target BG 110<br/>Override: Pump calculated Bolus = 0.0 units",
|
||||
"eventHistoryReportEventDesc": "Food Bolus: 50% Extended 15 mins",
|
||||
"foodBolusSize": 0.0,
|
||||
"iob": 5.87,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "631597",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": False
|
||||
},
|
||||
"requestDateTime": "2022-08-09T23:19:15",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-08-09T23:20:04",
|
||||
"value": 0.2
|
||||
},
|
||||
"foodDelivered": 0.0,
|
||||
"correctionDelivered": 0.0,
|
||||
"insulinRequested": 0.2,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3636.0,
|
||||
"bolusCompletionId": 3636.0
|
||||
},
|
||||
"standardPercent": 50.0,
|
||||
"targetBG": 110,
|
||||
"userOverride": 1,
|
||||
"type": "Bolus",
|
||||
"description": "Extended 50.00%/0.00",
|
||||
"sourceRecId": 1209631944,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
}
|
||||
|
||||
def test_extended_bolus_incomplete_to_bolus(self):
|
||||
e = BolusTherapyEvent.parse(self.extendedBolusIncompleteJson)
|
||||
self.assertIsNotNone(e)
|
||||
b = e.to_bolus()
|
||||
self.assertEqual(dataclasses.asdict(b), dataclasses.asdict(Bolus(
|
||||
description="Extended 50.00%/0.00",
|
||||
complete="0",
|
||||
completion="",
|
||||
request_time="2022-08-09 23:19:15-04:00",
|
||||
completion_time="2022-08-09 23:20:04-04:00",
|
||||
insulin="0.2",
|
||||
requested_insulin="0.2",
|
||||
carbs="0",
|
||||
bg="131",
|
||||
user_override="1",
|
||||
extended_bolus="1",
|
||||
bolex_completion_time="",
|
||||
bolex_start_time="2022-08-09 23:20:04-04:00"
|
||||
)))
|
||||
|
||||
extendedBolusJson = {
|
||||
"actualTotalBolusRequested": 0.4,
|
||||
"bg": 131,
|
||||
"bolex": {
|
||||
"size": 0.2,
|
||||
"bolexStartDateTime": "2022-08-09T23:20:04",
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-08-09T23:35:03",
|
||||
"value": 0.2
|
||||
},
|
||||
"iob": 5.7,
|
||||
"completionStatusId": 3.0,
|
||||
"completionStatusDesc": "Completed",
|
||||
"extendedBolusIsComplete": 1,
|
||||
"insulinRequested": 0.2,
|
||||
"bolexCompletionId": 16757133
|
||||
},
|
||||
"bolusRequestOptions": "Extended",
|
||||
"bolusType": "Carb",
|
||||
"carbSize": 0,
|
||||
"correctionBolusSize": 0.0,
|
||||
"correctionFactor": 30.0,
|
||||
"declinedCorrection": 0,
|
||||
"duration": 15,
|
||||
"eventDateTime": "2022-08-09T23:19:15",
|
||||
"eventHistoryReportDetails": "CF 1:30 - Carb Ratio 1:6 - Target BG 110<br/>Override: Pump calculated Bolus = 0.0 units",
|
||||
"eventHistoryReportEventDesc": "Food Bolus: 50% Extended 15 mins",
|
||||
"foodBolusSize": 0.0,
|
||||
"iob": 5.87,
|
||||
"isQuickBolus": 0,
|
||||
"note": {
|
||||
"id": 0,
|
||||
"indexId": "631597",
|
||||
"eventTypeId": 64,
|
||||
"sourceRecordId": 0,
|
||||
"eventId": 0,
|
||||
"active": False
|
||||
},
|
||||
"requestDateTime": "2022-08-09T23:19:15",
|
||||
"standard": {
|
||||
"insulinDelivered": {
|
||||
"completionDateTime": "2022-08-09T23:20:04",
|
||||
"value": 0.2
|
||||
},
|
||||
"foodDelivered": 0.0,
|
||||
"correctionDelivered": 0.0,
|
||||
"insulinRequested": 0.2,
|
||||
"completionStatusId": 3,
|
||||
"completionStatusDesc": "Completed",
|
||||
"bolusIsComplete": 1,
|
||||
"bolusRequestId": 3636.0,
|
||||
"bolusCompletionId": 3636.0
|
||||
},
|
||||
"standardPercent": 50.0,
|
||||
"targetBG": 110,
|
||||
"userOverride": 1,
|
||||
"type": "Bolus",
|
||||
"description": "Extended 50.00%/0.00",
|
||||
"sourceRecId": 1209631944,
|
||||
"eventTypeId": 0,
|
||||
"indexId": 0,
|
||||
"uploadId": 0,
|
||||
"interactive": 0,
|
||||
"tempRateId": 0,
|
||||
"tempRateCompleted": 0,
|
||||
"tempRateActivated": 0
|
||||
}
|
||||
|
||||
def test_extended_bolus_complete_to_bolus(self):
|
||||
e = BolusTherapyEvent.parse(self.extendedBolusJson)
|
||||
self.assertIsNotNone(e)
|
||||
b = e.to_bolus()
|
||||
self.assertEqual(dataclasses.asdict(b), dataclasses.asdict(Bolus(
|
||||
description="Extended 50.00%/0.00",
|
||||
complete="1",
|
||||
completion="Completed",
|
||||
request_time="2022-08-09 23:19:15-04:00",
|
||||
completion_time="2022-08-09 23:20:04-04:00",
|
||||
insulin="0.2",
|
||||
requested_insulin="0.2",
|
||||
carbs="0",
|
||||
bg="131",
|
||||
user_override="1",
|
||||
extended_bolus="1",
|
||||
bolex_completion_time="2022-08-09 23:35:03-04:00",
|
||||
bolex_start_time="2022-08-09 23:20:04-04:00"
|
||||
)))
|
||||
|
||||
|
||||
BOLUS_FULL_EXAMPLES = [
|
||||
TestBolusTherapyEvent.standardJson,
|
||||
TestBolusTherapyEvent.correctionJson,
|
||||
TestBolusTherapyEvent.extendedBolusJson
|
||||
]
|
||||
@@ -20,7 +20,7 @@ class NightscoutApi(tconnectsync.nightscout.NightscoutApi):
|
||||
def put_entry(self, ns_format, entity):
|
||||
self.put_entries[entity].append(ns_format)
|
||||
|
||||
def last_uploaded_entry(self, eventType):
|
||||
def last_uploaded_entry(self, eventType, start_date=None, end_date=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def last_uploaded_activity(self, activityType):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import unittest
|
||||
from tconnectsync.domain.bolus import Bolus
|
||||
from tconnectsync.parser.tconnect import TConnectEntry, UnknownBasalSuspensionEventException, UnknownCIQActivityEventException
|
||||
|
||||
class TestTConnectEntryBasal(unittest.TestCase):
|
||||
@@ -177,7 +178,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
def test_parse_bolus_entry_std_correction(self):
|
||||
self.assertEqual(
|
||||
TConnectEntry.parse_bolus_entry(self.entryStdCorrection),
|
||||
{
|
||||
Bolus(**{
|
||||
"description": "Standard/Correction",
|
||||
"complete": "1",
|
||||
"completion": "Completed",
|
||||
@@ -191,7 +192,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
"extended_bolus": "",
|
||||
"bolex_completion_time": None,
|
||||
"bolex_start_time": None
|
||||
})
|
||||
}))
|
||||
|
||||
entryStd = {
|
||||
"Type": "Bolus",
|
||||
@@ -239,7 +240,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
def test_parse_bolus_entry_std(self):
|
||||
self.assertEqual(
|
||||
TConnectEntry.parse_bolus_entry(self.entryStd),
|
||||
{
|
||||
Bolus(**{
|
||||
"description": "Standard",
|
||||
"complete": "1",
|
||||
"completion": "Completed",
|
||||
@@ -253,7 +254,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
"extended_bolus": "",
|
||||
"bolex_completion_time": None,
|
||||
"bolex_start_time": None
|
||||
})
|
||||
}))
|
||||
|
||||
entryStdAutomatic = {
|
||||
"Type": "Bolus",
|
||||
@@ -301,7 +302,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
def test_parse_bolus_entry_std_automatic(self):
|
||||
self.assertEqual(
|
||||
TConnectEntry.parse_bolus_entry(self.entryStdAutomatic),
|
||||
{
|
||||
Bolus(**{
|
||||
"description": "Automatic Bolus/Correction",
|
||||
"complete": "1",
|
||||
"completion": "Completed",
|
||||
@@ -315,7 +316,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
"extended_bolus": "",
|
||||
"bolex_completion_time": None,
|
||||
"bolex_start_time": None
|
||||
})
|
||||
}))
|
||||
|
||||
entryStdIncompleteZero = {
|
||||
"Type": "Bolus",
|
||||
@@ -363,7 +364,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
def test_parse_bolus_entry_std_incomplete_zero(self):
|
||||
self.assertEqual(
|
||||
TConnectEntry.parse_bolus_entry(self.entryStdIncompleteZero),
|
||||
{
|
||||
Bolus(**{
|
||||
"description": "Standard",
|
||||
"complete": "",
|
||||
"completion": "User Aborted",
|
||||
@@ -377,7 +378,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
"extended_bolus": "",
|
||||
"bolex_completion_time": None,
|
||||
"bolex_start_time": None
|
||||
})
|
||||
}))
|
||||
|
||||
entryStdIncompletePartial = {
|
||||
"Type": "Bolus",
|
||||
@@ -425,7 +426,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
def test_parse_bolus_entry_std_incomplete_partial(self):
|
||||
self.assertEqual(
|
||||
TConnectEntry.parse_bolus_entry(self.entryStdIncompletePartial),
|
||||
{
|
||||
Bolus(**{
|
||||
"description": "Standard/Correction",
|
||||
"complete": "",
|
||||
"completion": "Terminated by Alarm",
|
||||
@@ -439,7 +440,7 @@ class TestTConnectEntryBolus(unittest.TestCase):
|
||||
"extended_bolus": "",
|
||||
"bolex_completion_time": None,
|
||||
"bolex_start_time": None
|
||||
})
|
||||
}))
|
||||
|
||||
class TestTConnectEntryReading(unittest.TestCase):
|
||||
entry1 = {
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestBolusSync(unittest.TestCase):
|
||||
self.assertEqual(len(bolusEvents), len(bolusData))
|
||||
|
||||
def set_bg_type(entry, type):
|
||||
entry["bg_type"] = type
|
||||
entry.bg_type = type
|
||||
return entry
|
||||
|
||||
# Expect FINGER for bolus entries with a BG because there's no matching event with the same BG
|
||||
@@ -111,7 +111,7 @@ class TestBolusSync(unittest.TestCase):
|
||||
self.assertEqual(len(bolusEvents), len(bolusData))
|
||||
|
||||
def set_bg_type(entry, type):
|
||||
entry["bg_type"] = type
|
||||
entry.bg_type = type
|
||||
return entry
|
||||
|
||||
expected = [
|
||||
@@ -145,7 +145,7 @@ class TestBolusSync(unittest.TestCase):
|
||||
]
|
||||
|
||||
for e in partialEntries:
|
||||
e["description"] += " (%s: requested %s units)" % (e["completion"], e["requested_insulin"])
|
||||
e.description += " (%s: requested %s units)" % (e.completion, e.requested_insulin)
|
||||
|
||||
self.assertListEqual(bolusEvents, [
|
||||
TConnectEntry.parse_bolus_entry(d) for d in stdData
|
||||
|
||||
+65
-11
@@ -8,6 +8,7 @@ import copy
|
||||
from tconnectsync.process import process_time_range
|
||||
from tconnectsync.parser.nightscout import EXERCISE_EVENTTYPE, IOB_ACTIVITYTYPE, SLEEP_EVENTTYPE, NightscoutEntry
|
||||
from tconnectsync.features import BASAL, BOLUS, IOB, PUMP_EVENTS
|
||||
from tests.domain.test_therapy_event import BOLUS_FULL_EXAMPLES, TestCGMTherapyEvent
|
||||
|
||||
from .api.fake import TConnectApi
|
||||
from .nightscout_fake import NightscoutApi
|
||||
@@ -20,6 +21,11 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
|
||||
def stub_therapy_timeline(self, time_start, time_end):
|
||||
return copy.deepcopy(TestBasalSync.base)
|
||||
|
||||
def stub_ciq_therapy_events(self, time_start, time_end):
|
||||
return {
|
||||
"event": []
|
||||
}
|
||||
|
||||
def stub_therapy_timeline_csv(self, time_start, time_end):
|
||||
return {
|
||||
@@ -32,10 +38,10 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
def stub_ws2_basalsuspension(self, time_start, time_end):
|
||||
return {"BasalSuspension": []}
|
||||
|
||||
def stub_last_uploaded_entry(self, event_type):
|
||||
def stub_last_uploaded_entry(self, event_type, **kwargs):
|
||||
return None
|
||||
|
||||
def stub_last_uploaded_activity(self, activity_type):
|
||||
def stub_last_uploaded_activity(self, activity_type, **kwargs):
|
||||
return None
|
||||
|
||||
"""No data in Nightscout. Uploads all basal data from tconnect."""
|
||||
@@ -53,6 +59,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
return TestBasalSync.get_example_ciq_basal_events()
|
||||
|
||||
tconnect.controliq.therapy_timeline = fake_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
@@ -88,6 +95,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
return TestBasalSync.get_example_ciq_basal_events()
|
||||
|
||||
tconnect.controliq.therapy_timeline = fake_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
@@ -116,11 +124,12 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
return TestBasalSync.get_example_ciq_basal_events()
|
||||
|
||||
tconnect.controliq.therapy_timeline = fake_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
|
||||
def fake_last_uploaded_entry(event_type):
|
||||
def fake_last_uploaded_entry(event_type, **kwargs):
|
||||
if event_type == "Temp Basal":
|
||||
return {
|
||||
"created_at": "2021-03-16 00:20:21-04:00",
|
||||
@@ -159,11 +168,12 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
return TestBasalSync.get_example_ciq_basal_events()
|
||||
|
||||
tconnect.controliq.therapy_timeline = fake_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
|
||||
def fake_last_uploaded_entry(event_type):
|
||||
def fake_last_uploaded_entry(event_type, **kwargs):
|
||||
if event_type == "Temp Basal":
|
||||
return {
|
||||
"created_at": "2021-03-16 00:20:21-04:00",
|
||||
@@ -193,8 +203,8 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
})
|
||||
self.assertListEqual(nightscout.deleted_entries, [])
|
||||
|
||||
"""No data in Nightscout. Uploads all bolus data from tconnect."""
|
||||
def test_new_ciq_bolus_data(self):
|
||||
"""No data in Nightscout. Uploads all bolus data from tconnect via the WS2 API."""
|
||||
def test_new_ciq_bolus_data_from_ws2(self):
|
||||
tconnect = TConnectApi()
|
||||
|
||||
# datetimes are unused by the API fake
|
||||
@@ -202,6 +212,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
end = datetime.datetime(2021, 4, 21, 12, 0)
|
||||
|
||||
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
|
||||
bolusData = TestBolusSync.get_example_csv_bolus_events()
|
||||
def fake_therapy_timeline_csv(time_start, time_end):
|
||||
@@ -231,6 +242,46 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
self.assertDictEqual(nightscout.put_entries, {})
|
||||
self.assertListEqual(nightscout.deleted_entries, [])
|
||||
|
||||
"""No data in Nightscout. Uploads all bolus data from tconnect via CIQ therapy_events."""
|
||||
def test_new_ciq_bolus_data_from_ciq_therapy_events(self):
|
||||
tconnect = TConnectApi()
|
||||
|
||||
# datetimes are unused by the API fake
|
||||
start = datetime.datetime(2022, 8, 9, 12, 0)
|
||||
end = datetime.datetime(2021, 8, 10, 12, 0)
|
||||
|
||||
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
|
||||
|
||||
def fake_ciq_therapy_events(time_start, time_end):
|
||||
return {
|
||||
"event": BOLUS_FULL_EXAMPLES
|
||||
}
|
||||
tconnect.controliq.therapy_events = fake_ciq_therapy_events
|
||||
|
||||
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
|
||||
nightscout.last_uploaded_entry = self.stub_last_uploaded_entry
|
||||
nightscout.last_uploaded_activity = self.stub_last_uploaded_activity
|
||||
|
||||
process_time_range(tconnect, nightscout, start, end, pretend=False, features=[BOLUS, BASAL])
|
||||
|
||||
pprint.pprint(nightscout.uploaded_entries)
|
||||
self.assertEqual(len(nightscout.uploaded_entries["treatments"]), len(BOLUS_FULL_EXAMPLES))
|
||||
self.assertDictEqual(dict(nightscout.uploaded_entries), {
|
||||
"treatments": [
|
||||
# BGs are excluded because BG is not enabled in features for this test
|
||||
NightscoutEntry.bolus(2.9, 0, "2022-07-21 11:55:24-04:00", notes="Automatic Bolus/Correction"),
|
||||
NightscoutEntry.bolus(4.17, 25, "2022-07-21 12:29:21-04:00", notes="Standard"),
|
||||
# NOTE: the extended bolus is 0.2+0.2 but we currently only surface standard
|
||||
# BUG: inconsistency: we use the completion timestamp as the event timestamp for standard boluses,
|
||||
# but the standard strand time for extended
|
||||
NightscoutEntry.bolus(0.2, 0, "2022-08-09 23:20:04-04:00", notes="Extended 50.00%/0.00 (Override) (Extended)"),
|
||||
]})
|
||||
self.assertDictEqual(nightscout.put_entries, {})
|
||||
self.assertListEqual(nightscout.deleted_entries, [])
|
||||
|
||||
"""No data in Nightscout. Nothing should be updated in Nightscout without the BOLUS feature."""
|
||||
def test_bolus_data_not_updated_without_feature(self):
|
||||
tconnect = TConnectApi()
|
||||
@@ -271,6 +322,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
end = datetime.datetime(2021, 4, 21, 12, 0)
|
||||
|
||||
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
|
||||
iobData = TestIOBSync.get_example_csv_iob_events()
|
||||
def fake_therapy_timeline_csv(time_start, time_end):
|
||||
@@ -307,6 +359,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
end = datetime.datetime(2021, 4, 21, 12, 0)
|
||||
|
||||
tconnect.controliq.therapy_timeline = self.stub_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
|
||||
iobData = TestIOBSync.get_example_csv_iob_events()
|
||||
def fake_therapy_timeline_csv(time_start, time_end):
|
||||
@@ -354,7 +407,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
|
||||
nightscout.last_uploaded_entry = self.stub_last_uploaded_entry
|
||||
|
||||
def fake_last_uploaded_activity(activityType):
|
||||
def fake_last_uploaded_activity(activityType, **kwargs):
|
||||
if activityType == IOB_ACTIVITYTYPE:
|
||||
return iobData[0]
|
||||
return self.stub_last_uploaded_activity(activityType)
|
||||
@@ -454,6 +507,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
}
|
||||
|
||||
tconnect.controliq.therapy_timeline = fake_therapy_timeline
|
||||
tconnect.controliq.therapy_events = self.stub_ciq_therapy_events
|
||||
tconnect.ws2.therapy_timeline_csv = self.stub_therapy_timeline_csv
|
||||
tconnect.ws2.basalsuspension = self.stub_ws2_basalsuspension
|
||||
|
||||
@@ -506,7 +560,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
|
||||
def fake_last_uploaded_entry(event_type):
|
||||
def fake_last_uploaded_entry(event_type, **kwargs):
|
||||
if event_type == "Sleep":
|
||||
return {
|
||||
"created_at": "2021-05-02 14:46:40-04:00",
|
||||
@@ -519,7 +573,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
"duration": 20,
|
||||
"_id": "exercise"
|
||||
}
|
||||
return self.stub_last_uploaded_entry()
|
||||
return self.stub_last_uploaded_entry(**kwargs)
|
||||
|
||||
nightscout.last_uploaded_entry = fake_last_uploaded_entry
|
||||
nightscout.last_uploaded_activity = self.stub_last_uploaded_activity
|
||||
@@ -628,7 +682,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
|
||||
nightscout = NightscoutApi()
|
||||
|
||||
def fake_last_uploaded_entry(event_type):
|
||||
def fake_last_uploaded_entry(event_type, **kwargs):
|
||||
if event_type == "Site Change":
|
||||
return {
|
||||
"created_at": "2021-12-04 16:18:10-05:00"
|
||||
@@ -637,7 +691,7 @@ class TestProcessTimeRange(unittest.TestCase):
|
||||
return {
|
||||
"created_at": "2021-12-04 16:07:32-05:00"
|
||||
}
|
||||
return self.stub_last_uploaded_entry()
|
||||
return self.stub_last_uploaded_entry(**kwargs)
|
||||
|
||||
nightscout.last_uploaded_entry = fake_last_uploaded_entry
|
||||
nightscout.last_uploaded_activity = self.stub_last_uploaded_activity
|
||||
|
||||
Reference in New Issue
Block a user