Compare commits

...
11 Commits
7 changed files with 185 additions and 19 deletions
+42 -8
View File
@@ -46,9 +46,14 @@ The following synchronization feature is disabled by default, but can be enabled
After that, you can choose to run the program continuously via **Supervisord**
or on a regular interval with **Cron**.
**NOTE:** If you fork the tconnectsync repository on GitHub, **do not commit your .env file**.
If pushed to GitHub, this will make your tconnect and Nightscout passwords publicly visible and put your data at risk.
## Installation
First, create a file named `.env` containing configuration values.
First, you need to create a file containing configuration values.
The name of this file will be `.env`, and its location will be dependent on which
method of installation you choose.
You should specify the following parameters:
```bash
@@ -67,21 +72,31 @@ NS_SECRET='apisecret'
TIMEZONE_NAME='America/New_York'
```
These values can alternatively be specified via environment variables.
This file contains your t:connect username and password, Tandem pump serial number (which is utilized in API calls to t:connect), your Nightscout URL and secret token (for uploading data to Nightscout), and local timezone (the timezone used in t:connect).
The .env file contains your t:connect username and password, Tandem pump serial number (which is utilized in API calls to t:connect), your Nightscout URL and secret token (for uploading data to Nightscout), and local timezone (the timezone used in t:connect).
(Alternatively, these values can be specified via environment variables.)
### Installation via Pip
This is the easiest method to install.
First, ensure that you have **Python 3** with **Pip** installed on your
Linux machine. Then, install tconnectsync from pip:
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`
* **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.
Now install the `tconnectsync` package with pip:
```
$ pip3 install tconnectsync
```
If the pip3 command is not found, run `python3 -m pip install tconnectsync` instead.
After this, you should be able to view tconnectsync's help with:
```
$ tconnectsync --help
@@ -105,7 +120,12 @@ optional arguments:
Specifies what data should be synchronized between tconnect and Nightscout.
```
Go to the folder where you created the `.env` file, and run:
Move the `.env` file you created to the following folder:
* **MacOS:** `/Users/<username>/.config/tconnectsync/.env`
* **Linux:** `$HOME/.config/tconnectsync/.env`
* **Windows:** `$HOME/.config/tconnectsync/.env` (inside WSL)
```
$ tconnectsync --check-login
```
@@ -154,7 +174,8 @@ optional arguments:
```
Move the `.env` file you created earlier into this folder, and run:
Move the `.env` file you created earlier into the `tconnectsync` folder, and run:
```
$ pipenv run tconnectsync --check-login
```
@@ -172,6 +193,17 @@ $ docker pull ghcr.io/jwoglom/tconnectsync/tconnectsync:latest
$ docker run ghcr.io/jwoglom/tconnectsync/tconnectsync --help
```
Move the `.env` file you created earlier into the current folder, and run:
```
$ docker run tconnectsync --check-login
```
If you receive no errors, then you can move on to the **Running Tconnectsync Continuously** section.
#### Building Locally
To instead build the image locally and launch the project:
```bash
@@ -183,9 +215,11 @@ $ docker run tconnectsync --help
Move the `.env` file you created earlier into this folder, and run:
```
$ docker run tconnectsync --check-login
$ docker run --env-file=.env tconnectsync --check-login
```
**NOTE:** If using the `--env-file` option to `docker run`, you may need to remove all quotation marks (`'` and `"`s) around values in the `.env` file for Docker to propagate the variables correctly.
If you receive no errors, then you can move on to the **Running Tconnectsync Continuously** section.
## Running Tconnectsync Continuously
+2 -2
View File
@@ -1,6 +1,6 @@
[metadata]
name = tconnectsync
version = 0.6
version = 0.6.3
author = James Woglom
author_email = j@wogloms.net
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
@@ -34,4 +34,4 @@ exclude =
[options.entry_points]
console_scripts =
tconnectsync = tconnectsync:main
tconnectsync = tconnectsync:main
+1 -1
View File
@@ -30,7 +30,7 @@ except Exception:
__version__ = "UNKNOWN"
def parse_args(*args, **kwargs):
parser = argparse.ArgumentParser(description="Syncs bolus, basal, and IOB data from Tandem Diabetes t:connect to Nightscout.")
parser = argparse.ArgumentParser(description="Syncs bolus, basal, and IOB data from Tandem Diabetes t:connect to Nightscout.", epilog="Version %s" % __version__)
parser.add_argument('--version', action='version', version='tconnectsync %s' % __version__)
parser.add_argument('--pretend', dest='pretend', action='store_const', const=True, default=False, help='Pretend mode: do not upload any data to Nightscout.')
parser.add_argument('-v', '--verbose', dest='verbose', action='store_const', const=True, default=False, help='Verbose mode: show extra logging details')
+7 -3
View File
@@ -117,8 +117,9 @@ class TConnectEntry:
"bg": data["BG"], # Note: can be empty string for automatic Control-IQ boluses
"user_override": data["UserOverride"],
"extended_bolus": "1" if extended_bolus else "",
"bolex_completion_time": TConnectEntry._datetime_parse(data["BolexCompletionDateTime"]).format() if complete and extended_bolus else None,
"bolex_start_time": TConnectEntry._datetime_parse(data["BolexStartDateTime"]).format() if complete and extended_bolus else None,
# 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
@@ -156,7 +157,10 @@ class TConnectEntry:
"alarm": "Empty Cartridge/Pump Shutdown",
# manual corresponds to a Pumping Suspended by User event
"manual": "User Suspended"
"manual": "User Suspended",
# temp-profile corresponds to a Basal Rate Change event to 0u/hr
"temp-profile": "Basal Rate Change"
}
BASALSUSPENSION_SKIPPED_EVENTS = {
+15 -5
View File
@@ -1,10 +1,20 @@
import os, sys
from dotenv import load_dotenv
import os, sys, pathlib
from dotenv import dotenv_values
load_dotenv()
cwd_path = os.path.join(os.getcwd(), '.env')
global_path = os.path.join(pathlib.Path.home(), '.config/tconnectsync/.env')
def get(*args):
return os.environ.get(*args)
values = {}
if os.path.exists(cwd_path):
values = dotenv_values(cwd_path)
elif os.path.exists(global_path):
values = dotenv_values(global_path)
else:
values = dotenv_values()
def get(val, default=None):
return os.environ.get(val, values.get(val, default))
def get_number(name, default):
val = get(name, default)
+12
View File
@@ -594,6 +594,18 @@ class TestTConnectEntryBasalSuspensionEvent(unittest.TestCase):
}
)
def test_parse_basalsuspension_event_tempprofile(self):
self.assertEqual(
TConnectEntry.parse_basalsuspension_event({
'EventDateTime': '/Date(1640541521000-0000)/',
'SuspendReason': 'temp-profile'
}),
{
"time": "2021-12-26 09:58:41-05:00",
"event_type": "Basal Rate Change"
}
)
def test_parse_basalsuspension_event_basalprofile_skipped(self):
self.assertIsNone(
TConnectEntry.parse_basalsuspension_event({
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import unittest
import unittest.mock
import tempfile
import importlib
import contextlib
import pathlib
import os
@contextlib.contextmanager
def chdir(dir):
orig_cwd = os.getcwd()
os.chdir(dir)
try:
yield
finally:
os.chdir(orig_cwd)
class TestSecretDotEnv(unittest.TestCase):
maxDiff = None
def setUp(self):
if 'TCONNECT_EMAIL' in os.environ:
del os.environ['TCONNECT_EMAIL']
if 'NS_URL' in os.environ:
del os.environ['NS_URL']
def write_test_dotenv_file(self, path, type):
with open(os.path.join(path, ".env"), "w") as f:
f.write("""
TCONNECT_EMAIL=test_%s_email@email.com
NS_URL=http://test_%s_url
""" % (type, type))
f.close()
def import_secret(self):
return importlib.reload(importlib.import_module("tconnectsync.secret"))
def test_dotenv_in_current_working_directory(self):
with tempfile.TemporaryDirectory(prefix='dotenv_cwd') as dir, chdir(dir):
self.write_test_dotenv_file(dir, "dotenv_cwd")
secret = self.import_secret()
self.assertEqual(secret.TCONNECT_EMAIL, "test_dotenv_cwd_email@email.com")
self.assertEqual(secret.NS_URL, "http://test_dotenv_cwd_url")
def test_dotenv_in_homedir_config_folder(self):
with tempfile.TemporaryDirectory(prefix='dotenv_homedir_config') as dir, chdir(dir):
config_dir = os.path.join(dir, '.config/tconnectsync')
os.makedirs(config_dir)
self.write_test_dotenv_file(config_dir, "dotenv_homedir_config")
with unittest.mock.patch.object(pathlib.Path, "home") as mock_home:
mock_home.return_value = dir
secret = self.import_secret()
self.assertEqual(secret.TCONNECT_EMAIL, "test_dotenv_homedir_config_email@email.com")
self.assertEqual(secret.NS_URL, "http://test_dotenv_homedir_config_url")
def test_no_dotenv_file_reads_from_environment(self):
with tempfile.TemporaryDirectory(prefix='dotenv_environ') as dir, chdir(dir):
environ = {
"TCONNECT_EMAIL": "test_environ_email@email.com",
"NS_URL": "http://test_environ_url"
}
with unittest.mock.patch.dict(os.environ, environ):
secret = self.import_secret()
self.assertEqual(secret.TCONNECT_EMAIL, environ["TCONNECT_EMAIL"])
self.assertEqual(secret.NS_URL, environ["NS_URL"])
def test_environment_merges_with_dotenv_file(self):
with tempfile.TemporaryDirectory(prefix='env_plus_environ') as dir, chdir(dir):
self.write_test_dotenv_file(dir, "dotenv_cwd_defaults")
environ = {
"TCONNECT_EMAIL": "test_environ_override_email@email.com"
}
with unittest.mock.patch.dict(os.environ, environ):
secret = self.import_secret()
self.assertEqual(secret.TCONNECT_EMAIL, "test_environ_override_email@email.com")
self.assertEqual(secret.NS_URL, "http://test_dotenv_cwd_defaults_url")
def test_dotenv_in_current_working_directory_overrides_homedir_config(self):
with tempfile.TemporaryDirectory(prefix='dotenv_cwd') as dir, chdir(dir):
self.write_test_dotenv_file(dir, "dotenv_cwd")
config_dir = os.path.join(dir, '.config/tconnectsync')
os.makedirs(config_dir)
self.write_test_dotenv_file(config_dir, "dotenv_homedir_config")
with unittest.mock.patch.object(pathlib.Path, "home") as mock_home:
mock_home.return_value = dir
secret = self.import_secret()
self.assertEqual(secret.TCONNECT_EMAIL, "test_dotenv_cwd_email@email.com")
self.assertEqual(secret.NS_URL, "http://test_dotenv_cwd_url")
if __name__ == '__main__':
unittest.main()