mirror of
https://github.com/bckelley/tconnectsync.git
synced 2026-08-28 12:11:50 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d928c0ca72 | ||
|
|
5c09748fdb | ||
|
|
2bfc5a7860 | ||
|
|
73a0e2924d | ||
|
|
f973f374e7 | ||
|
|
8fb2ced9c5 | ||
|
|
d902cd7aab | ||
|
|
a781e458c3 | ||
|
|
3fe131bcf3 | ||
|
|
3260442893 | ||
|
|
e3542bc002 |
@@ -22,6 +22,16 @@ jobs:
|
||||
repository: jwoglom/tconnectsync/tconnectsync
|
||||
tag_with_ref: true
|
||||
|
||||
- name: Push latest tag to GitHub Packages
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: docker.pkg.github.com
|
||||
repository: jwoglom/tconnectsync/tconnectsync
|
||||
tags: latest
|
||||
push: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
push_to_dockerhub:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -35,7 +35,14 @@ jobs:
|
||||
pipenv check \
|
||||
--ignore 51499 \
|
||||
--ignore 52495 \
|
||||
--ignore 52365
|
||||
--ignore 52365 \
|
||||
--ignore 59956 \
|
||||
--ignore 58755 \
|
||||
--ignore 67895 \
|
||||
--ignore 61893 \
|
||||
--ignore 61601 \
|
||||
--ignore 62044 \
|
||||
--ignore 67599
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
|
||||
@@ -55,7 +55,7 @@ The following synchronization features can be optionally enabled:
|
||||
* Sleep Mode (in Nightscout, appears with a start and end time)
|
||||
* `IOB`: Insulin-on-board data. Only the most recent IOB entry is saved to Nightscout, as an "activity". The Nightscout UI does not currently display this information. In order to read this value, you need to query the Nightscout activity API endpoint. If you don't know what that means, then there is no reason to enable this option.
|
||||
|
||||
The following synchronization features are under development, [**but are not yet ready for use**](https://github.com/jwoglom/tconnectsync/issues/16):
|
||||
The following synchronization features are considered to be in alpha, and haven't been widely tested. If you want to use them, [set `ENABLE_TESTING_MODES=true` for them to show up](https://github.com/jwoglom/tconnectsync/blob/master/tconnectsync/features.py#L29):
|
||||
* `BOLUS_BG`: Adds BG readings which are associated with boluses on the pump into the Nightscout treatment object. It will determine whether the BG reading was automatically filled via the Dexcom connection on the pump or was manually entered by seeing if the BG reading matches the current CGM reading as known to the pump at that time. Support for this is nearly complete.
|
||||
* `CGM`: Adds Dexcom CGM readings from the pump to Nightscout as SGV (sensor glucose value) entries. This should only be used in a situation where xDrip/Dexcom Share/etc. is not used and the pump connection to the CGM will be the only source of CGM data to Nightscout. This requires additional testing before it should be considered ready.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 0.9.4
|
||||
version = 0.9.7
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -14,7 +14,7 @@ class ControlIQApi:
|
||||
BASE_URL = 'https://tdcservices.tandemdiabetes.com/'
|
||||
LOGIN_URL = 'https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2f'
|
||||
|
||||
LAST_CONFIRMED_SOFTWARE_VERSION = 't:connect 7.15.0.1'
|
||||
LAST_CONFIRMED_SOFTWARE_VERSION = 't:connect 7.16.0.1'
|
||||
|
||||
userGuid = None
|
||||
accessToken = None
|
||||
|
||||
@@ -30,10 +30,14 @@ class WebUIScraper:
|
||||
return self.controliq.needs_relogin()
|
||||
|
||||
def _get(self, endpoint):
|
||||
r = self.controliq.loginSession.get(self.BASE_URL + endpoint, headers=base_headers())
|
||||
r = self.controliq.loginSession.get(self.BASE_URL + endpoint, headers=base_headers(), allow_redirects=True)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise ApiException(r.status_code, "WebUIScraper HTTP %s response: %s" % (str(r.status_code), r.text))
|
||||
|
||||
if 'login.aspx' in r.url:
|
||||
raise ApiException(401, "WebUIScraper HTTP %s response for login page, returning 401: %s" % (str(r.status_code), r.url))
|
||||
|
||||
return r
|
||||
|
||||
|
||||
|
||||
@@ -192,11 +192,11 @@ def _ns_write_pump_events(nightscout, events, buildNsEventFunc, eventType, prete
|
||||
|
||||
add_count = 0
|
||||
for event in events:
|
||||
created_at = event["time"]
|
||||
if last_upload_time and arrow.get(created_at) <= last_upload_time:
|
||||
created_at = arrow.get(event["time"])
|
||||
if last_upload_time and created_at <= last_upload_time:
|
||||
skip = True
|
||||
if "duration_mins" in event.keys() and "duration" in last_upload.keys():
|
||||
if created_at == last_upload["created_at"] and float(event["duration_mins"]) > float(last_upload["duration"]):
|
||||
if created_at == arrow.get(last_upload["created_at"]) and float(event["duration_mins"]) > float(last_upload["duration"]):
|
||||
logger.info("Latest %s event needs updating: duration has increased from %s to %s: %s" % (eventType, last_upload["duration"], event["duration_mins"], event))
|
||||
logger.info("Deleting previous %s: %s" % (eventType, last_upload))
|
||||
nightscout.delete_entry('treatments/%s' % last_upload["_id"])
|
||||
|
||||
+59
-1
@@ -18,7 +18,7 @@ from .fake import ControlIQApi
|
||||
from tconnectsync.api.controliq import ControlIQApi as RealControlIQApi
|
||||
from tconnectsync.api.common import ApiException, ApiLoginException, base_headers
|
||||
|
||||
class TestWebUIScraper(unittest.TestCase):
|
||||
class TestWebUIScraperMyDevices(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
DEVICES_HTML = """
|
||||
@@ -319,6 +319,64 @@ class TestWebUIScraper(unittest.TestCase):
|
||||
'guid': None
|
||||
})})
|
||||
|
||||
REDIRECT_HTML = """<html><head><title>Object moved</title></head><body>
|
||||
<h2>Object moved to <a href="/login.aspx?ReturnUrl=%2fmyaccount%2fmy_devices.aspx">here</a>.</h2>
|
||||
</body></html>"""
|
||||
|
||||
LOGIN_HTML = """<html></html>"""
|
||||
|
||||
def test_relogin_when_redirected_to_login_page(self):
|
||||
ciq = ControlIQApi()
|
||||
ciq.needs_relogin = lambda: False
|
||||
ciq.loginSession = requests.Session()
|
||||
ciq.LOGIN_URL = RealControlIQApi.LOGIN_URL
|
||||
ciq._email = 'EMAIL'
|
||||
ciq._password = 'PASSWORD'
|
||||
|
||||
login_times = []
|
||||
def fake_login(email, password):
|
||||
self.assertEqual(email, ciq._email)
|
||||
self.assertEqual(password, ciq._password)
|
||||
|
||||
login_times.append(1)
|
||||
return True
|
||||
|
||||
ciq.login = fake_login
|
||||
webui = WebUIScraper(ciq)
|
||||
|
||||
ciq.login(ciq._email, ciq._password)
|
||||
self.assertEqual(len(login_times), 1)
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get('https://tconnect.tandemdiabetes.com/myaccount/my_devices.aspx',
|
||||
request_headers=base_headers(),
|
||||
|
||||
status_code=302,
|
||||
headers={'Location': 'https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2fmyaccount%2fmy_devices.aspx'},
|
||||
text=self.REDIRECT_HTML)
|
||||
|
||||
m.get('https://tconnect.tandemdiabetes.com/login.aspx?ReturnUrl=%2fmyaccount%2fmy_devices.aspx',
|
||||
request_headers=base_headers(),
|
||||
|
||||
status_code=200,
|
||||
text=self.LOGIN_HTML)
|
||||
|
||||
self.assertRaises(ApiException, webui.my_devices)
|
||||
self.assertEqual(len(login_times), 2)
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get('https://tconnect.tandemdiabetes.com/myaccount/my_devices.aspx',
|
||||
request_headers=base_headers(),
|
||||
|
||||
text=self.DEVICES_HTML)
|
||||
|
||||
devices = webui.my_devices()
|
||||
self.assertEqual(len(login_times), 2)
|
||||
|
||||
|
||||
class TestWebUIScraperPumpSettings(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
||||
PUMP_SETTINGS_HTML = """
|
||||
<!DOCTYPE html
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
|
||||
@@ -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, start_date=None, end_date=None):
|
||||
def last_uploaded_entry(self, eventType, time_start=None, time_end=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def last_uploaded_activity(self, activityType):
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from typing import Dict
|
||||
import copy
|
||||
|
||||
from tconnectsync.sync.pump_events import ns_write_pump_events
|
||||
|
||||
from ..nightscout_fake import NightscoutApi
|
||||
|
||||
|
||||
def _stub_last_uploaded_entry(eventType, time_start=None, time_end=None):
|
||||
return None
|
||||
|
||||
class TestNsWritePumpEvents(unittest.TestCase):
|
||||
|
||||
SITE_CHANGE_EVENT = {'time': '2024-02-04 16:27:50-05:00', 'event_type': 'Site/Cartridge Change'}
|
||||
SITE_CHANGE_NS = {'created_at': '2024-02-04 16:27:50-05:00', 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Site Change', 'notes': 'Site/Cartridge Change','reason': 'Site/Cartridge Change'}
|
||||
def test_write_single_sitechange_event(self):
|
||||
nightscout = NightscoutApi()
|
||||
nightscout.last_uploaded_entry = _stub_last_uploaded_entry
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.SITE_CHANGE_EVENT
|
||||
])
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.SITE_CHANGE_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
EMPTY_CART_EVENT = {'time': '2024-05-14 09:01:59-04:00', 'event_type': 'Empty Cartridge/Pump Shutdown'}
|
||||
EMPTY_CART_NS = {'created_at': '2024-05-14 09:01:59-04:00', 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Basal Suspension', 'notes': 'Empty Cartridge/Pump Shutdown', 'reason': 'Empty Cartridge/Pump Shutdown'}
|
||||
def test_write_single_emptycart_event(self):
|
||||
nightscout = NightscoutApi()
|
||||
nightscout.last_uploaded_entry = _stub_last_uploaded_entry
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.EMPTY_CART_EVENT
|
||||
])
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.EMPTY_CART_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
USER_SUSPENDED_EVENT = {'time': '2024-02-05 09:48:22-05:00', 'event_type': 'User Suspended'}
|
||||
USER_SUSPENDED_NS = {'created_at': '2024-02-05 09:48:22-05:00', 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Basal Suspension', 'notes': 'User Suspended', 'reason': 'User Suspended'}
|
||||
def test_write_single_usersuspended_event(self):
|
||||
nightscout = NightscoutApi()
|
||||
nightscout.last_uploaded_entry = _stub_last_uploaded_entry
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.USER_SUSPENDED_EVENT
|
||||
])
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.USER_SUSPENDED_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
EXERCISE_EVENT = {'time': '2024-03-10 08:53:20-04:00', 'duration_mins': 269.3833333333333, 'event_type': 'Exercise'}
|
||||
EXERCISE_NS = {'created_at': '2024-03-10 08:53:20-04:00', 'duration': 269.3833333333333, 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Exercise', 'notes': 'Exercise', 'reason': 'Exercise'}
|
||||
EXERCISE_EXTENDED_EVENT = {'time': '2024-03-10 08:53:20-04:00', 'duration_mins': 585.8833333333333, 'event_type': 'Exercise'}
|
||||
EXERCISE_EXTENDED_NS = {'created_at': '2024-03-10 08:53:20-04:00', 'duration': 585.8833333333333, 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Exercise', 'notes': 'Exercise', 'reason': 'Exercise'}
|
||||
def test_write_exercise_events(self):
|
||||
nightscout = NightscoutApi()
|
||||
nightscout.last_uploaded_entry = _stub_last_uploaded_entry
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.EXERCISE_EVENT
|
||||
])
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.EXERCISE_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
_id = 'exercise_id'
|
||||
def _last_uploaded_entry(eventType, time_start=None, time_end=None):
|
||||
if eventType == 'Exercise':
|
||||
return {
|
||||
'_id': _id,
|
||||
**self.EXERCISE_NS
|
||||
}
|
||||
|
||||
nightscout.last_uploaded_entry = _last_uploaded_entry
|
||||
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.EXERCISE_EXTENDED_EVENT
|
||||
])
|
||||
|
||||
self.assertListEqual([
|
||||
'treatments/%s' % _id
|
||||
], nightscout.deleted_entries)
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.EXERCISE_NS,
|
||||
self.EXERCISE_EXTENDED_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
SLEEP_EVENT = {'time': '2024-02-07 00:00:19-05:00', 'duration_mins': 13.916666666666666, 'event_type': 'Sleep'}
|
||||
SLEEP_NS = {'created_at': '2024-02-07 00:00:19-05:00', 'duration': 13.916666666666666, 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Sleep', 'notes': 'Sleep', 'reason': 'Sleep'}
|
||||
SLEEP_EXTENDED_EVENT = {'time': '2024-02-07 00:00:19-05:00', 'duration_mins': 74.0, 'event_type': 'Sleep'}
|
||||
SLEEP_EXTENDED_NS = {'created_at': '2024-02-07 00:00:19-05:00', 'duration': 74.0, 'enteredBy': 'Pump (tconnectsync)', 'eventType': 'Sleep', 'notes': 'Sleep', 'reason': 'Sleep'}
|
||||
def test_write_sleep_events(self):
|
||||
nightscout = NightscoutApi()
|
||||
nightscout.last_uploaded_entry = _stub_last_uploaded_entry
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.SLEEP_EVENT
|
||||
])
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.SLEEP_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
_id = 'sleep_id'
|
||||
def _last_uploaded_entry(eventType, time_start=None, time_end=None):
|
||||
if eventType == 'Sleep':
|
||||
return {
|
||||
'_id': _id,
|
||||
**self.SLEEP_NS
|
||||
}
|
||||
|
||||
nightscout.last_uploaded_entry = _last_uploaded_entry
|
||||
|
||||
ns_write_pump_events(nightscout, [
|
||||
self.SLEEP_EXTENDED_EVENT
|
||||
])
|
||||
|
||||
self.assertListEqual([
|
||||
'treatments/%s' % _id
|
||||
], nightscout.deleted_entries)
|
||||
|
||||
self.assertDictEqual({
|
||||
'treatments': [
|
||||
self.SLEEP_NS,
|
||||
self.SLEEP_EXTENDED_NS
|
||||
]
|
||||
}, dict(nightscout.uploaded_entries))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user