mirror of
https://github.com/jwoglom/tconnectsync.git
synced 2026-08-27 02:34:13 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4ab6398f0 | ||
|
|
f67671e1ef | ||
|
|
5e4d81a618 | ||
|
|
cabbc783c7 | ||
|
|
62f1322ef6 | ||
|
|
139ccb1878 | ||
|
|
e60e9ce537 | ||
|
|
6e180d9f2a | ||
|
|
b929d1151e | ||
|
|
4d4ee7b425 | ||
|
|
0c5a6d5453 | ||
|
|
0b7e33db96 | ||
|
|
5d58b23d27 | ||
|
|
40aea810cc | ||
|
|
ec84afdb7a | ||
|
|
fca7f573a3 | ||
|
|
76fc043a2b |
@@ -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.
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 50%
|
||||
threshold: null
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 0.8.3
|
||||
version = 0.8.9
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -429,3 +429,52 @@ class BolusTherapyEvent(TherapyEvent):
|
||||
'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
|
||||
@@ -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
@@ -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).isoformat()
|
||||
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()
|
||||
@@ -1,4 +1,4 @@
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent, BasalTherapyEvent
|
||||
from tconnectsync.parser.tconnect import TConnectEntry
|
||||
|
||||
import logging
|
||||
@@ -9,6 +9,7 @@ def split_therapy_events(ciqTherapyEvents):
|
||||
bolusEvents = []
|
||||
cgmEvents = []
|
||||
bgEvents = []
|
||||
basalEvents = []
|
||||
for e in ciqTherapyEvents['event']:
|
||||
event = TConnectEntry.parse_therapy_event(e)
|
||||
if isinstance(event, BolusTherapyEvent):
|
||||
@@ -17,7 +18,10 @@ def split_therapy_events(ciqTherapyEvents):
|
||||
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, %d BG" % (len(bolusEvents), len(cgmEvents), len(bgEvents)))
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
import arrow
|
||||
from tconnectsync.domain.bolus import Bolus
|
||||
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent
|
||||
from tconnectsync.domain.therapy_event import BolusTherapyEvent, CGMTherapyEvent, BGTherapyEvent, BasalTherapyEvent
|
||||
|
||||
try:
|
||||
from ..secret import TIMEZONE_NAME
|
||||
@@ -202,6 +202,8 @@ class TConnectEntry:
|
||||
return CGMTherapyEvent.parse(data)
|
||||
elif data["type"] == "BG":
|
||||
return BGTherapyEvent.parse(data)
|
||||
elif data["type"] == "Basal":
|
||||
return BasalTherapyEvent.parse(data)
|
||||
|
||||
raise UnknownTherapyEventException(data)
|
||||
|
||||
@@ -215,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
@@ -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
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -34,6 +34,12 @@ def process_bolus_events(bolusdata, cgmEvents=None, source=""):
|
||||
else:
|
||||
logger.warning("Skipping non-completed %s bolus data (was a bolus in progress?): %s parsed: %s" % (source, b, parsed))
|
||||
continue
|
||||
if parsed.is_extended_bolus:
|
||||
if not parsed.bolex_start_time and not parsed.start_time:
|
||||
logger.warning("Skipping non-completed %s extended bolus data with no start time: %s parsed: %s" % (source, b, parsed))
|
||||
elif not parsed.bolex_start_time and parsed.start_time:
|
||||
logger.warning("Setting bolex_start_time to start_time for non-completed %s extended bolus: %s parsed: %s" % (source, b, parsed))
|
||||
parsed.bolex_start_time = parsed.start_time
|
||||
|
||||
if parsed.bg and cgmEvents:
|
||||
requested_at = parsed.request_time if not parsed.extended_bolus else parsed.bolex_start_time
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user