Compare commits

..
19 Commits
Author SHA1 Message Date
James Woglom 5e4d81a618 v0.8.8 2022-12-12 22:37:30 -05:00
James Woglom cabbc783c7 catch api exceptions due to workaround with timestamp format in nightscout (#71) 2022-12-12 22:36:46 -05:00
James Woglom 62f1322ef6 v0.8.7 2022-12-02 20:48:51 -05:00
James Woglom 139ccb1878 ignore python3.7 pipenv check warning 2022-12-02 20:48:12 -05:00
James Woglom e60e9ce537 version 0.8.6 2022-12-02 20:42:35 -05:00
James Woglom 6e180d9f2a add basal therapy event type, now that Tandem is storing it along with bolus/cgm/bgs 2022-12-02 20:42:35 -05:00
Jarred YawandJames Woglom b929d1151e Moved logger.warn() statements to logger.warning() due to the deprecation msgs I was getting while testing 2022-10-02 00:18:11 -04:00
James Woglom 4d4ee7b425 v0.8.5 2022-10-02 00:07:32 -04:00
James Woglom 0c5a6d5453 when last_uploaded query returns None, try with fixed timestamp 2022-10-02 00:05:08 -04:00
James Woglom 0b7e33db96 allow self-signed SSL cert with NS_SKIP_TLS_VERIFY (fixes #63) 2022-10-01 23:49:16 -04:00
James Woglom 5d58b23d27 v0.8.4 2022-09-17 10:31:23 -04:00
James Woglom 40aea810cc pass kwargs to ws2 query 2022-09-17 10:30:01 -04:00
Ryan StutsmanandJames Woglom ec84afdb7a Fix format string in UnknownTherapyEventType.
%s was missing from the format string. This enhances the message to
report the unexpected thearpy type along with the dump of the event that
trigger the exception.
2022-08-26 10:53:50 -04:00
James Woglom fca7f573a3 set codecov threshold to 50%, to stop failing builds for 0.01% changes in code coverage 2022-08-26 10:53:38 -04:00
James WoglomandGitHub 76fc043a2b Update issue templates 2022-08-24 12:55:32 -04:00
James Woglom 86e07880a9 bump to v0.8.3 2022-08-24 12:28:43 -04:00
Jarred YawandJames Woglom 751087373c fix unknown therapy event error in ciq_therapy_event.py 2022-08-24 12:20:45 -04:00
Ryan StutsmanandJames Woglom f47421e482 Replace hand-rolled date format with isoformat().
Fixes #47.
2022-08-22 16:45:46 -04:00
t1diotacandJames Woglom dfc39d61bb Update README.md
- Added sections on creating a user-specific install and crontab.
- Added bullet points on installing on CentOS/RHEL/Rocky Linux 8
2022-08-19 10:31:39 -04:00
19 changed files with 430 additions and 113 deletions
@@ -0,0 +1,53 @@
---
name: 'Setup help: tconnectsync-heroku'
about: When experiencing an issue setting up tconnectsync-heroku
title: ''
labels: heroku, setup help
assignees: ''
---
**Describe the problem**
A clear and concise description of the issue you're experiencing.
**To Reproduce**
Steps to reproduce the behavior:
1. ...
2. ...
**Expected behavior**
A clear and concise description of what you expected to happen.
**Have you followed the [Troubleshooting steps in the README?](https://github.com/jwoglom/tconnectsync-heroku#troubleshooting)** (Yes or No)
**Setup details**
* **On what platform are you using the t:connect mobile app?** (Android or iOS)
* **What version are you using of the t:connect mobile app?**
**Heroku log output**
If applicable, add the full output from the heroku logs in More > View Logs. ([See the instructions in the README.](https://github.com/jwoglom/tconnectsync-heroku#testing))
**Check Login output**
If applicable, [please follow the instructions to visit the `check_login` page](https://github.com/jwoglom/tconnectsync-heroku#testing), and copy and paste the output here.
This file may contain sensitive details like your Nightscout URL and pump serial number. Copy the output into a text editor like Textedit or Notepad first and find-and-replace sensitive strings, like your Nightscout URL and pump serial number, if they appear.
The output will also contain pump and CGM related information such as delivery events and blood sugars. If you would prefer to keep the contents private, send an email to tconnectsync<at>wogloms.net with the log output so @jwoglom can investigate.
**Additional context**
Add any other context about the problem here.
@@ -0,0 +1,49 @@
---
name: 'Setup help: tconnectsync'
about: When experiencing an issue setting up tconnectsync on Windows, Linux, or MacOS.
title: ''
labels: setup help
assignees: ''
---
**Describe the problem**
A clear and concise description of the issue you're experiencing.
**To Reproduce**
Steps to reproduce the behavior:
1. ...
2. ...
**Expected behavior**
A clear and concise description of what you expected to happen.
**Setup details**
* **Operating system:**
* **tconnectsync version** (from `--version`):
* **On what platform are you using the t:connect mobile app?** (Android or iOS)
* **What version are you using of the t:connect mobile app?**
**Terminal output**
If applicable, add the full output from your terminal from running tconnectsync. Copy the output into a text editor like Textedit or Notepad first and find-and-replace sensitive strings, like your Nightscout URL and pump serial number, if they appear.
**Check Login output**
If applicable, please run `--check-login` and upload the tconnectsync-check-output.log file as an attachment on the issue. This file should have sensitive details like your Nightscout URL and pump serial number automatically removed, but double-check before uploading.
This log will contain pump and CGM related information such as delivery events and blood sugars. If you would prefer to keep the contents private, send an email to tconnectsync<at>wogloms.net with the log output so @jwoglom can investigate.
**Additional context**
Add any other context about the problem here.
+2 -1
View File
@@ -30,7 +30,8 @@ jobs:
pipenv install --system
- name: Run pipenv check
run: |
pipenv check
pipenv check \
--ignore 51499 # DDoS attack in wheel package, which is unsupported in python 3.7
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Generated
+39 -39
View File
@@ -16,18 +16,18 @@
"default": {
"arrow": {
"hashes": [
"sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b",
"sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"
"sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1",
"sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"
],
"index": "pypi",
"version": "==1.2.2"
"version": "==1.2.3"
},
"beautifulsoup4": {
"hashes": [
"sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30",
"sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"
],
"markers": "python_full_version >= '3.6.0'",
"markers": "python_version >= '3.6'",
"version": "==4.11.1"
},
"bs4": {
@@ -39,27 +39,27 @@
},
"certifi": {
"hashes": [
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
"sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14",
"sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"
],
"markers": "python_full_version >= '3.6.0'",
"version": "==2022.6.15"
"markers": "python_version >= '3.6'",
"version": "==2022.9.24"
},
"charset-normalizer": {
"hashes": [
"sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5",
"sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
],
"markers": "python_full_version >= '3.6.0'",
"version": "==2.1.0"
"markers": "python_version >= '3.6'",
"version": "==2.1.1"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.3"
"version": "==3.4"
},
"lxml": {
"hashes": [
@@ -156,11 +156,11 @@
},
"python-dotenv": {
"hashes": [
"sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f",
"sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"
"sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5",
"sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"
],
"index": "pypi",
"version": "==0.20.0"
"version": "==0.21.0"
},
"requests": {
"hashes": [
@@ -172,11 +172,11 @@
},
"requests-mock": {
"hashes": [
"sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970",
"sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"
"sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699",
"sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b"
],
"index": "pypi",
"version": "==1.9.3"
"version": "==1.10.0"
},
"six": {
"hashes": [
@@ -191,20 +191,20 @@
"sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
"sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
],
"markers": "python_full_version >= '3.6.0'",
"markers": "python_version >= '3.6'",
"version": "==2.3.2.post1"
},
"tconnectsync": {
"path": ".",
"version": "==0.7.1"
"version": "==0.8.6"
},
"urllib3": {
"hashes": [
"sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec",
"sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"
"sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc",
"sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
"version": "==1.26.10"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==1.26.13"
}
},
"develop": {
@@ -217,11 +217,11 @@
},
"jedi": {
"hashes": [
"sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d",
"sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"
"sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e",
"sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"
],
"markers": "python_version >= '3.6'",
"version": "==0.18.1"
"version": "==0.18.2"
},
"parso": {
"hashes": [
@@ -233,27 +233,27 @@
},
"prompt-toolkit": {
"hashes": [
"sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0",
"sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"
"sha256:535c29c31216c77302877d5120aef6c94ff573748a5b5ca5b1b1f76f5e700c73",
"sha256:ced598b222f6f4029c0800cefaa6a17373fb580cd093223003475ce32805c35b"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.0.30"
"version": "==3.0.33"
},
"ptpython": {
"hashes": [
"sha256:99636899ab0e4d026e2ecc9368269114f387b4bb5411e57f072b0bde724d9f99",
"sha256:eafd4ced27ca5dc370881d4358d1ab5041b32d88d31af8e3c24167fe4af64ed6"
"sha256:1ec28ba37959e8e6c4880fcf4748ad5c61341d4c1bfd24a38a4d5b97af1f475a",
"sha256:a57b9952eac4a12540a4dfb4ccd4a243403eceb27b0d13245b5e4144c8731d32"
],
"index": "pypi",
"version": "==3.0.20"
"version": "==3.0.21"
},
"pygments": {
"hashes": [
"sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb",
"sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"
"sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1",
"sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"
],
"markers": "python_version >= '3.6'",
"version": "==2.12.0"
"version": "==2.13.0"
},
"wcwidth": {
"hashes": [
+17 -3
View File
@@ -116,7 +116,10 @@ First, ensure that you have **Python 3** with **Pip** installed:
* **On MacOS:** Open Terminal. Install [Homebrew](https://brew.sh/), and then run `brew install python3`
* **On Linux:** Follow your distribution's specific instructions.
For Debian/Ubuntu based distros, `sudo apt install python3 python3-pip`
- For Debian/Ubuntu based distros, `sudo apt install python3 python3-pip`
- For CentOS/Rocky Linux 8:
- `sudo dnf install python39-pip`
- `sudo alternatives --set python /usr/bin/python3.9`
* **On Windows:** Install Ubuntu under the [Windows Subsystem for Linux](https://ubuntu.com/wsl).
Open the Ubuntu Terminal, then run `sudo apt install python3 python3-pip`.
Perform the remainder of the steps under the Ubuntu environment.
@@ -126,6 +129,12 @@ Now install the `tconnectsync` package with pip:
```
$ pip3 install tconnectsync
```
To install into a user environment instead of system-wide for a more contained install:
````
$ pip3 install --user tconnectsync
````
- This will place the tconnectsync binary file at ``/home/<username>/.local/bin/tconnectsync``
If the pip3 command is not found, run `python3 -m pip install tconnectsync` instead.
@@ -377,13 +386,18 @@ invoking tconnectsync with no arguments via cron.
If using Pipenv or a virtualenv, make sure that you either prefix the call to main.py with `pipenv run` or source the `bin/activate` file within the virtualenv, so that the proper dependencies are loaded. If not using any kind of virtualenv, you can instead just install the necessary dependencies as specified inside Pipfile globally.
An example configuration in `/etc/crontab` which runs every 15 minutes:
An system-wide example configuration in `/etc/crontab` which runs every 15 minutes, on the 15 minute mark:
```bash
# m h dom mon dow user command
0,15,30,45 * * * * root /path/to/tconnectsync/run.sh
```
An example of a user crontab `crontab -e` if not running system-wide, which runs every 15 minutes:
```
*/15 * * * * /path/to/tconnectsync/run.sh
```
You can use one of the same `run.sh` files referenced above, but remove the `--auto-update` flag since you are handling the functionality for running the script periodically yourself.
## Tandem APIs
@@ -417,4 +431,4 @@ tconnectsync.util.cli.enable_logging()
api = tconnectsync.util.cli.get_api()
# Make API calls, e.g.
therapy_timeline = api.controliq.therapy_timeline('2022-08-01', '2022-08-10')
```
```
+6
View File
@@ -0,0 +1,6 @@
coverage:
status:
project:
default:
target: 50%
threshold: null
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = tconnectsync
version = 0.8.2
version = 0.8.8
author = James Woglom
author_email = j@wogloms.net
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
+3 -2
View File
@@ -17,7 +17,8 @@ try:
TCONNECT_EMAIL,
TCONNECT_PASSWORD,
NS_URL,
NS_SECRET
NS_SECRET,
NS_SKIP_TLS_VERIFY
)
from . import secret
except Exception:
@@ -74,7 +75,7 @@ def main(*args, **kwargs):
tconnect = TConnectApi(TCONNECT_EMAIL, TCONNECT_PASSWORD)
nightscout = NightscoutApi(NS_URL, NS_SECRET)
nightscout = NightscoutApi(NS_URL, NS_SECRET, NS_SKIP_TLS_VERIFY)
if args.check_login:
return check_login(tconnect, time_start, time_end)
+3 -3
View File
@@ -55,10 +55,10 @@ class ControlIQApi:
self.tconnect_software_ver = version
logger.info("Reported tconnect software version: %s" % version)
if version != self.LAST_CONFIRMED_SOFTWARE_VERSION:
logger.warn("Newer API version than last confirmed working. Saw %s and expected %s" % (version, self.LAST_CONFIRMED_SOFTWARE_VERSION))
logger.warn("If you experience any issues, please report them to https://github.com/jwoglom/tconnectsync")
logger.warning("Newer API version than last confirmed working. Saw %s and expected %s" % (version, self.LAST_CONFIRMED_SOFTWARE_VERSION))
logger.warning("If you experience any issues, please report them to https://github.com/jwoglom/tconnectsync")
except Exception:
logger.warn("Unable to find tconnect software version")
logger.warning("Unable to find tconnect software version")
pass
return {
"__LASTFOCUS": "",
+2 -2
View File
@@ -21,8 +21,8 @@ class WS2Api:
self.userGuid = userGuid
self.session = base_session()
def get(self, endpoint):
r = self.session.get(self.BASE_URL + endpoint, headers=base_headers())
def get(self, endpoint, **kwargs):
r = self.session.get(self.BASE_URL + endpoint, headers=base_headers(), **kwargs)
if r.status_code != 200:
raise ApiException(r.status_code, "WS2 API HTTP %s response: %s" % (str(r.status_code), r.text))
return r.text
+3 -3
View File
@@ -76,7 +76,7 @@ class Autoupdate:
logger.error("Exiting with error code due to AUTOUPDATE_RESTART_ON_FAILURE")
return 1
else:
logger.warn(AutoupdateFailureWarning(("%s: An event index change was recorded, but no new data was found via the API. " % datetime.datetime.now()) +
logger.warning(AutoupdateFailureWarning(("%s: An event index change was recorded, but no new data was found via the API. " % datetime.datetime.now()) +
"The %s was %d minutes ago. Resetting TConnectApi to attempt to solve this problem." %
("last processed event" if self.last_successful_process_time_range else "start of autoupdate", (now - last_action_or_start)//60)))
@@ -143,7 +143,7 @@ class Autoupdate:
# uploaded, so
if len(self.time_diffs_between_attempts) >= 3:
# The pump hasn't sent us data that, based on previous cadence, we were expecting
logger.warn(AutoupdateNoIndexChangeWarning("Sleeping %d seconds after unexpected no index change based on previous cadence. (New data might be delayed.)" %
logger.warning(AutoupdateNoIndexChangeWarning("Sleeping %d seconds after unexpected no index change based on previous cadence. (New data might be delayed.)" %
int(self.secret.AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS)))
logger.debug("Last event time: %s, time diffs between attempts: %s" % (self.last_event_time, self.time_diffs_between_attempts))
@@ -204,4 +204,4 @@ class AutoupdateNoNewDataDetectedError(AutoupdateError):
pass
class AutoupdateNoIndexChangeWarning(AutoupdateWarning):
pass
pass
+115 -2
View File
@@ -51,12 +51,52 @@ class CGMTherapyEvent(TherapyEvent):
},
"""
@classmethod
def parse(self, json):
def parse(_, json):
self = CGMTherapyEvent()
TherapyEvent.parse(self, json)
self.eventID = json['eventID']
self.egv = json['egv']['estimatedGlucoseValue']
return self
class BGTherapyEvent(TherapyEvent):
eventID = None
egv = None
"""
{
'bg': 160, # note in EGV
'cgmCalibration': 1, # not in EGV
'description': 'BG',
'deviceType': 't:slim X2 Insulin Pump',
'eventDateTime': '2022-08-20T07:25:24',
'eventTypeId': 16,
'indexId': 844955,
'interactive': 0,
'iob': 0.75,
'note': { 'active': False,
'eventId': 0, # different location than EGV
'eventTypeId': 16,
'id': 0,
'indexId': '',
'sourceRecordId': 0},
'requestDateTime': '0001-01-01T00:00:00',
'serialNumber': 'xxx',
'sourceRecId': 793549667,
'tempRateActivated': 0,
'tempRateCompleted': 0,
'tempRateId': 0,
'type': 'BG',
'uploadId': 748700213}
"""
@classmethod
def parse(_, json):
self = BGTherapyEvent()
TherapyEvent.parse(self, json)
self.eventID = json['note']['eventId']
# This is probably not how we want to provide CGM calibrations to Nightscout,
# but will just include it as egv data for now to keep the thing from crashing :)
self.egv = json['bg']
return self
class BolusTherapyEvent(TherapyEvent):
bolusRequestOptions = None
REQUEST_AUTOMATIC = "Automatic Bolus/Correction"
@@ -364,4 +404,77 @@ class BolusTherapyEvent(TherapyEvent):
"tempRateCompleted": 0,
"tempRateActivated": 0
}
"""
CGM Calibration (Therapy Event Type BG):
{ 'bg': 160,
'cgmCalibration': 1,
'description': 'BG',
'deviceType': 't:slim X2 Insulin Pump',
'eventDateTime': '2022-08-20T07:25:24',
'eventTypeId': 16,
'indexId': 844955,
'interactive': 0,
'iob': 0.75,
'note': { 'active': False,
'eventId': 0,
'eventTypeId': 16,
'id': 0,
'indexId': '',
'sourceRecordId': 0},
'requestDateTime': '0001-01-01T00:00:00',
'serialNumber': 'xxx',
'sourceRecId': 793549667,
'tempRateActivated': 0,
'tempRateCompleted': 0,
'tempRateId': 0,
'type': 'BG',
'uploadId': 0}
"""
class BasalTherapyEvent(TherapyEvent):
"""
{
'basalRate': {
'duration': 0,
'percent': 0,
'value': 0.0
},
'displayInHistory': 0,
'eventDateTime': '2022-12-02T00:00:00',
'note': {
'id': 0,
'indexId': '16403',
'eventTypeId': 90,
'sourceRecordId': 0,
'eventId': 0,
'active': False
},
'noteDate': {},
'requestDateTime': '0001-01-01T00:00:00',
'type': 'Basal',
'description': 'NDE',
'sourceRecId': xxx,
'eventTypeId': 0,
'indexId': 0,
'uploadId': 0,
'interactive': 1,
'tempRateId': 0,
'tempRateCompleted': 0,
'tempRateActivated': 0
}
"""
basalRateValue = None
basalRatePercent = None
basalRateDuration = None
eventTime = None
@classmethod
def parse(_, json):
self = CGMTherapyEvent()
TherapyEvent.parse(self, json)
if 'basalRate' in json:
self.basalRateValue = json['basalRate']['value']
self.basalRatePercent = json['basalRate']['percent']
self.basalRateDuration = json['basalRate']['duration']
self.eventTime = json['eventDateTime']
return self
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
class Time:
def __init__(self, hour: int, min: int):
self.hour = hour
self.min = min
@classmethod
def parse(cls, input):
if ' ' not in input:
raise ValueError('unable to parse time: %s' % input)
hrmin, ampm = input.split(' ')
hr, min = hrmin.split(':')
hr = int(hr)
min = int(min)
if ampm.lower() == 'pm':
hr += 12
elif ampm.lower() != 'am':
raise ValueError('unable to parse time: %s' % input)
return cls(hr, min)
+80 -39
View File
@@ -4,6 +4,7 @@ import hashlib
import time
import urllib.parse
import arrow
import logging
from urllib.parse import urljoin
@@ -11,21 +12,28 @@ from .api.common import ApiException
from .parser.nightscout import ENTERED_BY
def format_datetime(date):
return arrow.get(date).strftime('%Y-%m-%d %H:%M:%S')
return arrow.get(date).isoformat()
def time_range(field_name, start_time, end_time):
def time_range(field_name, start_time, end_time, t_to_space=False):
def fmt(date):
ret = format_datetime(date)
if t_to_space:
return ret.replace('T', ' ')
return ret
arg = ''
if start_time:
arg += '&find[%s][$gte]=%s' % (field_name, format_datetime(start_time))
arg += '&find[%s][$gte]=%s' % (field_name, fmt(start_time))
if end_time:
arg += '&find[%s][$lte]=%s' % (field_name, format_datetime(end_time))
arg += '&find[%s][$lte]=%s' % (field_name, fmt(end_time))
return arg
logger = logging.getLogger(__name__)
class NightscoutApi:
def __init__(self, url, secret):
def __init__(self, url, secret, skip_verify=False):
self.url = url
self.secret = secret
self.verify = False if skip_verify else None
def upload_entry(self, ns_format, entity='treatments'):
@@ -33,7 +41,7 @@ class NightscoutApi:
'Accept': 'application/json',
'Content-Type': 'application/json',
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
})
}, verify=self.verify)
if r.status_code != 200:
raise ApiException(r.status_code, "Nightscout upload response: %s" % r.text)
@@ -42,7 +50,7 @@ class NightscoutApi:
'Accept': 'application/json',
'Content-Type': 'application/json',
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
})
}, verify=self.verify)
if r.status_code != 200:
raise ApiException(r.status_code, "Nightscout delete response: %s" % r.text)
@@ -51,48 +59,81 @@ class NightscoutApi:
'Accept': 'application/json',
'Content-Type': 'application/json',
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
})
}, verify=self.verify)
if r.status_code != 200:
raise ApiException(r.status_code, "Nightscout put response: %s" % r.text)
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:
raise ApiException(latest.status_code, "Nightscout last_uploaded_entry response: %s" % latest.text)
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/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()
}, verify=self.verify)
if latest.status_code != 200:
raise ApiException(latest.status_code, "Nightscout last_uploaded_entry response: %s" % latest.text)
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
ret = None
try:
ret = internal(False)
except ApiException as e:
logger.warning("last_uploaded_entry with no t_to_space: %s", e)
ret = None
if ret is None and (time_start or time_end):
try:
ret = internal(True)
except ApiException as e:
logger.warning("last_uploaded_entry with t_to_space: %s", e)
ret = None
if ret is not None:
logger.warning("last_uploaded_entry with eventType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (eventType, time_start, time_end))
return ret
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:
raise ApiException(latest.status_code, "Nightscout last_uploaded_bg_entry response: %s" % latest.text)
def internal(t_to_space):
dateFilter = time_range('dateString', time_start, time_end, t_to_space=t_to_space)
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()
}, verify=self.verify)
if latest.status_code != 200:
raise ApiException(latest.status_code, "Nightscout last_uploaded_bg_entry response: %s" % latest.text)
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
ret = internal(False)
if ret is None and (time_start or time_end):
ret = internal(True)
if ret is not None:
logger.warning("last_uploaded_bg_entry with time_start=%s time_end=%s only returned data when timestamps contained a space" % (time_start, time_end))
return ret
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:
raise ApiException(latest.status_code, "Nightscout activity response: %s" % latest.text)
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/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()
}, verify=self.verify)
if latest.status_code != 200:
raise ApiException(latest.status_code, "Nightscout activity response: %s" % latest.text)
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
j = latest.json()
if j and len(j) > 0:
return j[0]
return None
ret = internal(False)
if ret is None and (time_start or time_end):
ret = internal(True)
if ret is not None:
logger.warning("last_uploaded_activity with activityType=%s time_start=%s time_end=%s only returned data when timestamps contained a space" % (activityType, time_start, time_end))
return ret
"""
Returns general status information about the Nightscout server.
@@ -100,7 +141,7 @@ class NightscoutApi:
def api_status(self):
status = requests.get(urljoin(self.url, 'api/v1/status.json'), headers={
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
})
}, verify=self.verify)
if status.status_code != 200:
raise Exception('HTTP error status code (%d) from Nightscout: %s' % (status.status_code, status.text))
return status.json()
+12 -4
View File
@@ -1,4 +1,4 @@
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent, BasalTherapyEvent
from tconnectsync.parser.tconnect import TConnectEntry
import logging
@@ -8,12 +8,20 @@ logger = logging.getLogger(__name__)
def split_therapy_events(ciqTherapyEvents):
bolusEvents = []
cgmEvents = []
bgEvents = []
basalEvents = []
for e in ciqTherapyEvents['event']:
event = TConnectEntry.parse_therapy_event(e)
if type(event) == BolusTherapyEvent:
if isinstance(event, BolusTherapyEvent):
bolusEvents.append(event)
elif type(event) == CGMTherapyEvent:
elif isinstance(event, CGMTherapyEvent):
cgmEvents.append(event)
elif isinstance(event, BGTherapyEvent):
bgEvents.append(event)
elif isinstance(event, BasalTherapyEvent):
basalEvents.append(event)
logger.debug("split_therapy_events: %d bolus, %d CGM" % (len(bolusEvents), len(cgmEvents)))
logger.debug("split_therapy_events: %d bolus, %d CGM, %d BG, %d basal" % (len(bolusEvents), len(cgmEvents), len(bgEvents), len(basalEvents)))
# TODO: BG events (CGM Calibration) values are not currently returned from ciq_therapy_events.py
return bolusEvents, cgmEvents
+7 -2
View File
@@ -3,7 +3,7 @@ import sys
import arrow
from tconnectsync.domain.bolus import Bolus
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent, BasalTherapyEvent
try:
from ..secret import TIMEZONE_NAME
@@ -200,6 +200,10 @@ class TConnectEntry:
return BolusTherapyEvent.parse(data)
elif data["type"] == "CGM":
return CGMTherapyEvent.parse(data)
elif data["type"] == "BG":
return BGTherapyEvent.parse(data)
elif data["type"] == "Basal":
return BasalTherapyEvent.parse(data)
raise UnknownTherapyEventException(data)
@@ -213,4 +217,5 @@ class UnknownBasalSuspensionEventException(Exception):
class UnknownTherapyEventException(Exception):
def __init__(self, data):
super().__init__("Unknown therapy event type: " % data)
typ = data["type"]
super().__init__(f"Unknown therapy event type: {typ} in {data}")
+10 -10
View File
@@ -86,19 +86,19 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
ciqFallingBack or \
BOLUS_BG in features or \
IOB in features:
logger.warn("Downloading t:connect CSV data")
logger.warning("Downloading t:connect CSV data")
if bolusFallingBack:
logger.warn("Falling back on WS2 CSV data source because BOLUS is an enabled feature and CIQ bolus data was empty!!")
logger.warning("Falling back on WS2 CSV data source because BOLUS is an enabled feature and CIQ bolus data was empty!!")
if ciqFallingBack:
logger.warn("Falling back on WS2 CSV data source because CGM is an enabled feature and CIQ cgm data was empty!!")
logger.warning("Falling back on WS2 CSV 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 CSV data source because BOLUS_BG is an enabled feature. " +
logger.warning("Falling back on WS2 CSV 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 CSV data source because IOB is an enabled feature. " +
logger.warning("Falling back on WS2 CSV data source because IOB is an enabled feature. " +
"Please consider disabling this feature to improve synchronization reliability.")
logger.warn("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
logger.warning("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
csvdata = tconnect.ws2.therapy_timeline_csv(time_start, time_end)
csvReadingData = csvdata["readingData"]
@@ -147,8 +147,8 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
pumpEvents = process_ciq_activity_events(ciqTherapyTimelineData)
logger.debug("CIQ activity events: %s" % pumpEvents)
logger.warn("Using WS2 data source for basalsuspension because PUMP_EVENTS is an enabled feature")
logger.warn("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
logger.warning("Using WS2 data source for basalsuspension because PUMP_EVENTS is an enabled feature")
logger.warning("<!!> The WS2 data source is unreliable and may prevent timely synchronization")
ws2BasalSuspension = tconnect.ws2.basalsuspension(time_start, time_end)
bsPumpEvents = process_basalsuspension_events(ws2BasalSuspension)
@@ -167,7 +167,7 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
bolusEvents = process_bolus_events(ciqBolusData, source="ciq")
if csvBolusData and not bolusEvents:
logger.warn("Falling back on non-CIQ csvBolusData")
logger.warning("Falling back on non-CIQ csvBolusData")
bolusEvents = process_bolus_events(csvBolusData, source="csv")
logger.debug("ciq bolusEvents: %s" % bolusEvents)
@@ -184,4 +184,4 @@ def process_time_range(tconnect, nightscout, time_start, time_end, pretend, feat
logger.debug("Finished writing iob events")
logger.info("Wrote %d events to Nightscout this process cycle" % added)
return added
return added
+2
View File
@@ -40,6 +40,8 @@ if not get('NS_SECRET') and get('API_SECRET'):
print('API_SECRET environment variable is set, overriding NS_SECRET')
NS_SECRET = get('API_SECRET')
NS_SKIP_TLS_VERIFY = get_bool('NS_SKIP_TLS_VERIFY', 'false')
TIMEZONE_NAME = get('TIMEZONE_NAME', 'America/New_York')
if not get('TIMEZONE_NAME') and get('TZ'):
+2 -2
View File
@@ -24,9 +24,9 @@ def build_mock_logger():
logger.error(*args, **kwargs)
def fake_warn(*args, **kwargs):
logger.warn(*args, **kwargs)
logger.warning(*args, **kwargs)
with patch("tconnectsync.autoupdate.logger.error") as mock_error, patch("tconnectsync.autoupdate.logger.warn") as mock_warn:
with patch("tconnectsync.autoupdate.logger.error") as mock_error, patch("tconnectsync.autoupdate.logger.warning") as mock_warn:
mock_error.side_effect = fake_error
mock_warn.side_effect = fake_warn
yield (mock_error, mock_warn)