mirror of
https://github.com/jwoglom/tconnectsync.git
synced 2026-08-27 10:13:39 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38c933a257 | ||
|
|
d507e47e7a | ||
|
|
8c2f41c2d5 | ||
|
|
018c6d2228 | ||
|
|
32ba49dc8a | ||
|
|
a5cbc5d612 | ||
|
|
ef515021cc | ||
|
|
4af54bf7cb | ||
|
|
d9d1b8a8fb | ||
|
|
29ed755356 | ||
|
|
b862ea9d0d | ||
|
|
2677fc86be | ||
|
|
1ae40e3cdb | ||
|
|
90c63c8211 | ||
|
|
60c2e08dcf |
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10']
|
||||
python-version: ['3.8', '3.9', '3.10', '3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -9,17 +9,18 @@ ptpython = "*"
|
||||
[packages]
|
||||
tconnectsync = {path = "."}
|
||||
bs4 = "*"
|
||||
arrow = "*"
|
||||
arrow = "==1.2.3"
|
||||
lxml = "*"
|
||||
python-dotenv = "*"
|
||||
python-dotenv = "==0.21.1"
|
||||
requests-mock = "*"
|
||||
pysocks = "*"
|
||||
urllib3 = "==1.26.6"
|
||||
requests = {extras = ["socks"], version = "*"}
|
||||
requests = {extras = ["socks"], version = "==2.31.0"}
|
||||
requests-oidc = "*"
|
||||
pyjwt = "*"
|
||||
cryptography = "*"
|
||||
dataclasses-json = "*"
|
||||
cffi = ">=1.15.1"
|
||||
|
||||
[scripts]
|
||||
tconnectsync = "python3 main.py"
|
||||
|
||||
Generated
+15
-22
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "586b477d4c73da1d1034f852a536bf3c20a1a358406bfb5dfc1b1164f606441b"
|
||||
"sha256": "74ba9aa4c7c93f3533b986f868b63bea54995dbd8d37bde4745bc9023de66dc7"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -16,12 +16,12 @@
|
||||
"default": {
|
||||
"arrow": {
|
||||
"hashes": [
|
||||
"sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80",
|
||||
"sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"
|
||||
"sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1",
|
||||
"sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.3.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.2.3"
|
||||
},
|
||||
"beautifulsoup4": {
|
||||
"hashes": [
|
||||
@@ -117,7 +117,8 @@
|
||||
"sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
|
||||
"sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
|
||||
],
|
||||
"markers": "platform_python_implementation != 'PyPy'",
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.17.1"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
@@ -504,12 +505,12 @@
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca",
|
||||
"sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"
|
||||
"sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49",
|
||||
"sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.0.1"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.21.1"
|
||||
},
|
||||
"qrcode": {
|
||||
"hashes": [
|
||||
@@ -524,11 +525,11 @@
|
||||
"socks"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
|
||||
"sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
|
||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.3"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.31.0"
|
||||
},
|
||||
"requests-mock": {
|
||||
"hashes": [
|
||||
@@ -584,14 +585,6 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.9.4"
|
||||
},
|
||||
"types-python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6",
|
||||
"sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.9.0.20240906"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 2.0.1
|
||||
version = 2.0.4
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem t:connect pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -111,9 +111,14 @@ def main(*args, **kwargs):
|
||||
|
||||
if args.auto_update:
|
||||
u = TandemSourceAutoupdate(secret)
|
||||
sys.exit(u.process(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features))
|
||||
changed, last_event_id = u.process(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features)
|
||||
|
||||
# return exit code 0 if processed events
|
||||
sys.exit(0 if changed>0 else 1)
|
||||
else:
|
||||
tconnectDevice = TandemSourceChooseDevice(secret, tconnect).choose()
|
||||
added = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, features=args.features).process(time_start, time_end)
|
||||
sys.exit(added)
|
||||
|
||||
# return exit code 0 if processed events
|
||||
sys.exit(0 if added>0 else 1)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import struct
|
||||
import arrow
|
||||
|
||||
from ..secret import TIMEZONE_NAME
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
EVENT_LEN = 26
|
||||
@@ -29,7 +31,11 @@ class RawEvent:
|
||||
seqNum = seqNum,
|
||||
raw = raw
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return arrow.get(TANDEM_EPOCH + self.timestampRaw)
|
||||
# Event timestamps do not have TZ data attached to them when parsed,
|
||||
# but represent the user's time zone setting. So we keep the time
|
||||
# referenced on them, but force the timezone to what the user
|
||||
# requests via the TZ secret.
|
||||
return arrow.get(TANDEM_EPOCH + self.timestampRaw, tzinfo=TIMEZONE_NAME)
|
||||
|
||||
@@ -18,6 +18,7 @@ class TandemSourceAutoupdate:
|
||||
self.last_max_date_with_events = None
|
||||
self.last_event_time = 0
|
||||
self.last_attempt_time = 0
|
||||
self.last_event_index = None
|
||||
self.time_diffs_between_attempts = []
|
||||
self.time_diffs_between_updates = []
|
||||
|
||||
@@ -41,15 +42,17 @@ class TandemSourceAutoupdate:
|
||||
|
||||
tconnectDevice = ChooseDevice(self.secret, tconnect).choose()
|
||||
|
||||
cur_max_date_with_events = arrow.get(tconnectDevice['maxDateWithEvents'])
|
||||
if not self.last_max_date_with_events or cur_max_date_with_events > self.cur_max_date_with_events:
|
||||
event_id = None
|
||||
cur_max_date_with_events = arrow.get(tconnectDevice['maxDateWithEvents']).float_timestamp
|
||||
if not self.last_max_date_with_events or cur_max_date_with_events > self.last_max_date_with_events:
|
||||
logger.info('New reported tandemsource data. (cur_max_date: %s last_max_date: %s)' % (cur_max_date_with_events, self.last_max_date_with_events))
|
||||
|
||||
if pretend:
|
||||
logger.info('Would update now if not in pretend mode')
|
||||
else:
|
||||
added = ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, features=features).process(time_start, time_end)
|
||||
added, event_id = ProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend, features=features).process(time_start, time_end)
|
||||
logger.info('Added %d items from ProcessTimeRange' % added)
|
||||
self.last_successful_process_time_range = now
|
||||
|
||||
# Track the time it took to find a new event between runs,
|
||||
# but skip this calculation the first process cycle (since
|
||||
@@ -59,8 +62,10 @@ class TandemSourceAutoupdate:
|
||||
logger.debug('Updating tracking of time since last update: %s' % self.time_diffs_between_updates)
|
||||
|
||||
# Mark the last event index uploaded from the pump and timestamp
|
||||
if event_id:
|
||||
self.last_event_index = event_id
|
||||
self.last_event_time = now
|
||||
self.last_max_date_with_events = cur_max_date_with_events
|
||||
self.last_event_time = now
|
||||
self.last_attempt_time = now
|
||||
self.time_diffs_between_attempts = []
|
||||
else:
|
||||
|
||||
@@ -46,23 +46,27 @@ class ProcessTimeRange:
|
||||
]
|
||||
|
||||
def process(self, time_start, time_end):
|
||||
logger.info(f"ProcessTimeRange {time_start=} {time_end=} {self.tconnect_device_id=} {self.features=}")
|
||||
logger.info(f"ProcessTimeRange time_start={time_start} time_end={time_end} tconnect_device_id={self.tconnect_device_id} features={self.features}")
|
||||
|
||||
pump_events_raw = self.tconnect.tandemsource.pump_events_raw(self.tconnect_device_id, time_start, time_end)
|
||||
pump_events_decoded = decode_raw_events(pump_events_raw)
|
||||
logger.info(f"Read {len(pump_events_decoded)=} (est. {len(pump_events_decoded)/EVENT_LEN} events)")
|
||||
logger.info(f"Read {len(pump_events_decoded)} bytes (est. {len(pump_events_decoded)/EVENT_LEN} events)")
|
||||
events = Events(pump_events_decoded)
|
||||
|
||||
events_first_time = None
|
||||
events_last_time = None
|
||||
last_event_id = None # aka seqnum
|
||||
for_eventclass = collections.defaultdict(list)
|
||||
for event in events:
|
||||
if not events_first_time:
|
||||
events_first_time = event.eventTimestamp
|
||||
if not events_last_time:
|
||||
events_last_time = event.eventTimestamp
|
||||
if not last_event_id:
|
||||
last_event_id = event.eventId
|
||||
events_first_time = min(events_first_time, event.eventTimestamp)
|
||||
events_last_time = max(events_last_time, event.eventTimestamp)
|
||||
last_event_id = max(event.eventId, last_event_id)
|
||||
|
||||
clazz = EventClass.for_event(event)
|
||||
if clazz:
|
||||
@@ -91,6 +95,6 @@ class ProcessTimeRange:
|
||||
else:
|
||||
logger.info("Skipping %s, is not enabled from features %s" % (updater_class.__name__, self.features))
|
||||
|
||||
logger.info("Processed %d events" % processed_count)
|
||||
return processed_count
|
||||
logger.info("Processed %d events. Last event ID seen: %d" % (processed_count, last_event_id))
|
||||
return processed_count, last_event_id
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ class ProcessAlarm:
|
||||
|
||||
ns_entries = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping Alarm event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
logger.info("Skipping Alarm event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
ns_entries.append(self.alarm_to_nsentry(event))
|
||||
|
||||
@@ -36,13 +36,17 @@ class ProcessBasal:
|
||||
|
||||
with_duration = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping basal event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
logger.info("Skipping basal event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
with_duration.append([event.eventTimestamp, None, event])
|
||||
|
||||
if not with_duration:
|
||||
logger.info("No basal events found to process")
|
||||
return []
|
||||
|
||||
for i in range(len(with_duration)-1):
|
||||
with_duration[i][1] = with_duration[i+1][0] - with_duration[i][0]
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ class ProcessBasalResume:
|
||||
|
||||
ns_entries = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping BasalResume event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
logger.info("Skipping BasalResume event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
ns_entries.append(self.resume_to_nsentry(event))
|
||||
|
||||
@@ -35,9 +35,9 @@ class ProcessBasalSuspension:
|
||||
|
||||
ns_entries = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping basalsuspension event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
logger.info("Skipping basalsuspension event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
ns_entries.append(self.suspension_to_nsentry(event))
|
||||
|
||||
@@ -44,7 +44,7 @@ class ProcessBolus:
|
||||
bolusEventsForId[event.bolusid][type(event)] = event
|
||||
|
||||
if type(event) == eventtypes.LidBolusCompleted:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping bolusCompletedEvent before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
@@ -37,9 +37,9 @@ class ProcessCartridge:
|
||||
cannulaFilledEvents = []
|
||||
tubingFilledEvents = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
logger.info("Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
continue
|
||||
|
||||
if type(event) == eventtypes.LidCartridgeFilled:
|
||||
|
||||
@@ -35,9 +35,9 @@ class ProcessCGMAlert:
|
||||
|
||||
alertEvents = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
logger.info("Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
continue
|
||||
|
||||
alertEvents.append(event)
|
||||
|
||||
@@ -38,9 +38,9 @@ class ProcessCGMReading:
|
||||
|
||||
readings = []
|
||||
for event in sorted(events, key=lambda x: self.timestamp_for(x)):
|
||||
if last_upload_time and self.timestamp_for(event) < last_upload_time:
|
||||
if last_upload_time and self.timestamp_for(event) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("ProcessCGMReading: Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
logger.info("ProcessCGMReading: Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
continue
|
||||
|
||||
readings.append(event)
|
||||
|
||||
@@ -48,9 +48,9 @@ class ProcessCGMStartJoinStop:
|
||||
|
||||
allEvents = []
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("ProcessCGMStartJoinStop: Skipping %s before last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
logger.info("ProcessCGMStartJoinStop: Skipping %s not after last upload time: %s (time range: %s - %s)" % (type(event), event, time_start, time_end))
|
||||
continue
|
||||
|
||||
allEvents.append(event)
|
||||
|
||||
@@ -72,9 +72,9 @@ class ProcessUserMode:
|
||||
start_sleep = None
|
||||
start_exercise = None
|
||||
for event in sorted(events, key=lambda x: x.eventTimestamp):
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) < last_upload_time:
|
||||
if last_upload_time and arrow.get(event.eventTimestamp) <= last_upload_time:
|
||||
if self.pretend:
|
||||
logger.info("ProcessUserMode: Skipping usermode event before last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
logger.info("ProcessUserMode: Skipping usermode event not after last upload time: %s (time range: %s - %s)" % (event, time_start, time_end))
|
||||
continue
|
||||
|
||||
if self.is_start_sleep(event):
|
||||
|
||||
Reference in New Issue
Block a user