mirror of
https://github.com/jwoglom/tconnectsync.git
synced 2026-08-26 10:13:24 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea3f6bd6b | ||
|
|
2bf40cac28 | ||
|
|
73dabe5ead | ||
|
|
e848a8482d | ||
|
|
9c06efb045 | ||
|
|
64aeb4d752 | ||
|
|
6a9a426b97 | ||
|
|
5a76a4cfd1 |
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = tconnectsync
|
||||
version = 2.3.0
|
||||
version = 2.3.3
|
||||
author = James Woglom
|
||||
author_email = j@wogloms.net
|
||||
description = Syncs Tandem Source (formerly t:connect) insulin pump data to Nightscout for the t:slim X2
|
||||
|
||||
@@ -119,7 +119,7 @@ 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))
|
||||
sys.exit(u.process(tconnect, nightscout, args.pretend, features=args.features))
|
||||
else:
|
||||
tconnectDevice = TandemSourceChooseDevice(secret, tconnect).choose()
|
||||
added, last_event_id = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, secret=secret, features=args.features).process(time_start, time_end)
|
||||
|
||||
@@ -27,7 +27,7 @@ class TandemSourceAutoupdate:
|
||||
until stopped (ctrl+c), or a maximum of AUTOUPDATE_MAX_LOOP_INVOCATIONS times.
|
||||
Stops if AUTOUPDATE_RESTART_ON_FAILURE is set and an error occurs.
|
||||
"""
|
||||
def process(self, tconnect, nightscout, time_start, time_end, pretend, features=None):
|
||||
def process(self, tconnect, nightscout, pretend, features=None):
|
||||
if features is None:
|
||||
features = DEFAULT_FEATURES
|
||||
|
||||
@@ -40,6 +40,9 @@ class TandemSourceAutoupdate:
|
||||
logger.debug("autoupdate loop")
|
||||
now = time.time()
|
||||
|
||||
time_end = datetime.datetime.now()
|
||||
time_start = time_end - datetime.timedelta(days=1)
|
||||
|
||||
tconnectDevice = ChooseDevice(self.secret, tconnect).choose()
|
||||
|
||||
event_seqnum = None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import collections
|
||||
import arrow
|
||||
|
||||
from ...features import DEVICE_STATUS, DEFAULT_FEATURES
|
||||
from ...eventparser import events as eventtypes
|
||||
@@ -81,7 +82,11 @@ class ProcessTimeRange:
|
||||
c = self.event_classes[clazz](self.tconnect, self.nightscout, self.tconnect_device_id, self.pretend, self.features)
|
||||
if c.enabled():
|
||||
logger.info("%s is enabled from features %s" % (clazz, self.features))
|
||||
ns_entries = c.process(events, events_first_time, events_last_time)
|
||||
# Cap events_last_time at time_end to handle pump clock drift
|
||||
# Ensure time_end is timezone-aware for comparison
|
||||
time_end_aware = arrow.get(time_end)
|
||||
capped_time_end = min(events_last_time, time_end_aware) if events_last_time else time_end_aware
|
||||
ns_entries = c.process(events, events_first_time, capped_time_end)
|
||||
w = c.write(ns_entries)
|
||||
if w:
|
||||
processed_count += w
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import unittest
|
||||
import arrow
|
||||
|
||||
from tconnectsync.sync.tandemsource.process import ProcessTimeRange
|
||||
from tconnectsync.eventparser import events as eventtypes
|
||||
from tconnectsync.eventparser.generic import Event
|
||||
|
||||
from ...api.fake import TConnectApi
|
||||
from ...nightscout_fake import NightscoutApi
|
||||
from ...secrets import build_secrets
|
||||
|
||||
# Raw event bytes for testing
|
||||
# LidBasalDelivery (id=279) at 2025-11-18 13:12:40-05:00, rate=800 milliunits
|
||||
BASAL_EVENT_1 = b'\x01\x17!\xa2\xeeH\x00\x01\x86\xa1\x00\x00\x00\x03\x03 \x03 \x00\x00\x03 \x00\x00\x00\x00'
|
||||
|
||||
# LidBasalDelivery (id=279) at 2025-11-18 13:17:40-05:00, rate=800 milliunits
|
||||
BASAL_EVENT_2 = b'\x01\x17!\xa2\xeft\x00\x01\x86\xa2\x00\x00\x00\x03\x03 \x03 \x00\x00\x03 \x00\x00\x00\x00'
|
||||
|
||||
# LidCgmDataG7 (id=399) at 2025-11-19 03:00:00-05:00 (future timestamp for testing clock drift)
|
||||
CGM_EVENT_FUTURE = b'\x01\x8f!\xa3\xb00\x00\x03\rA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
# LidCgmDataG7 (id=399) at 2025-11-18 13:22:40-05:00 (normal timestamp)
|
||||
CGM_EVENT_NORMAL = b'\x01\x8f!\xa2\xf0\xa0\x00\x03\rB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
|
||||
|
||||
class FakeTandemSourceApi:
|
||||
"""Fake TandemSource API for testing"""
|
||||
def __init__(self):
|
||||
self.events = []
|
||||
|
||||
def pump_events(self, device_id, time_start, time_end, fetch_all_event_types=False):
|
||||
return self.events
|
||||
|
||||
def pump_event_metadata(self):
|
||||
"""Return empty metadata for testing"""
|
||||
return {}
|
||||
|
||||
def needs_relogin(self):
|
||||
return False
|
||||
|
||||
|
||||
class TestProcessTimeRangeBasalDuration(unittest.TestCase):
|
||||
"""Test that basal duration calculation caps events_last_time at time_end"""
|
||||
maxDiff = None
|
||||
|
||||
def setUp(self):
|
||||
self.tconnect = TConnectApi()
|
||||
self.tconnect._tandemsource = FakeTandemSourceApi()
|
||||
self.nightscout = NightscoutApi()
|
||||
self.nightscout.last_uploaded_entry = lambda *args, **kwargs: None
|
||||
|
||||
self.tconnectDevice = {
|
||||
'tconnectDeviceId': 'test-device-123',
|
||||
'maxDateWithEvents': '2025-11-18T13:00:00-05:00'
|
||||
}
|
||||
|
||||
self.secret = build_secrets(
|
||||
FETCH_ALL_EVENT_TYPES=False
|
||||
)
|
||||
|
||||
self.process = ProcessTimeRange(
|
||||
self.tconnect,
|
||||
self.nightscout,
|
||||
self.tconnectDevice,
|
||||
pretend=False,
|
||||
secret=self.secret
|
||||
)
|
||||
|
||||
def test_basal_duration_capped_when_future_event_timestamp(self):
|
||||
"""Test that basal duration is capped at time_end when last event is in the future"""
|
||||
|
||||
# Create events from raw bytes
|
||||
basal_event_1 = Event(BASAL_EVENT_1) # 2025-11-18 13:12:40-05:00
|
||||
basal_event_2 = Event(BASAL_EVENT_2) # 2025-11-18 13:17:40-05:00
|
||||
future_cgm_event = Event(CGM_EVENT_FUTURE) # 2025-11-19 03:00:00-05:00 (future)
|
||||
|
||||
self.assertEqual(type(basal_event_1), eventtypes.LidBasalDelivery)
|
||||
self.assertEqual(type(basal_event_2), eventtypes.LidBasalDelivery)
|
||||
self.assertEqual(type(future_cgm_event), eventtypes.LidCgmDataG7)
|
||||
|
||||
# Set up the fake API to return these events
|
||||
self.tconnect._tandemsource.events = [basal_event_1, basal_event_2, future_cgm_event]
|
||||
|
||||
# time_end is "now" at 13:29:00
|
||||
time_start = arrow.get('2025-11-18T13:00:00-05:00')
|
||||
time_end = arrow.get('2025-11-18T13:29:00-05:00')
|
||||
|
||||
# Process the events
|
||||
count, last_seqnum = self.process.process(time_start, time_end)
|
||||
|
||||
# Verify that basal events were uploaded
|
||||
self.assertEqual(len(self.nightscout.uploaded_entries['treatments']), 2)
|
||||
|
||||
# First basal: from 13:12:40 to 13:17:40 = 5 minutes
|
||||
basal_1 = self.nightscout.uploaded_entries['treatments'][0]
|
||||
self.assertEqual(basal_1['eventType'], 'Temp Basal')
|
||||
self.assertEqual(basal_1['created_at'], '2025-11-18 13:12:40-05:00')
|
||||
self.assertEqual(basal_1['duration'], 5.0)
|
||||
|
||||
# Second basal: from 13:17:40 to time_end (13:29:00) = 11.333... minutes
|
||||
# NOT from 13:17:40 to future_cgm_event (03:00:00) which would be ~822 minutes
|
||||
basal_2 = self.nightscout.uploaded_entries['treatments'][1]
|
||||
self.assertEqual(basal_2['eventType'], 'Temp Basal')
|
||||
self.assertEqual(basal_2['created_at'], '2025-11-18 13:17:40-05:00')
|
||||
# Duration should be capped at time_end, not extended to future CGM event
|
||||
expected_duration = (time_end - arrow.get('2025-11-18T13:17:40-05:00')).seconds / 60
|
||||
self.assertAlmostEqual(basal_2['duration'], expected_duration, places=2)
|
||||
# Verify it's NOT the inflated duration to the future event
|
||||
self.assertLess(basal_2['duration'], 100) # Should be ~11 min, not ~822 min
|
||||
|
||||
def test_basal_duration_normal_when_all_events_in_past(self):
|
||||
"""Test that basal duration uses events_last_time when it's <= time_end"""
|
||||
|
||||
# Create events from raw bytes
|
||||
basal_event_1 = Event(BASAL_EVENT_1) # 2025-11-18 13:12:40-05:00
|
||||
basal_event_2 = Event(BASAL_EVENT_2) # 2025-11-18 13:17:40-05:00
|
||||
cgm_event = Event(CGM_EVENT_NORMAL) # 2025-11-18 13:22:40-05:00 (normal)
|
||||
|
||||
self.assertEqual(type(basal_event_1), eventtypes.LidBasalDelivery)
|
||||
self.assertEqual(type(basal_event_2), eventtypes.LidBasalDelivery)
|
||||
self.assertEqual(type(cgm_event), eventtypes.LidCgmDataG7)
|
||||
|
||||
# Set up the fake API to return these events
|
||||
self.tconnect._tandemsource.events = [basal_event_1, basal_event_2, cgm_event]
|
||||
|
||||
time_start = arrow.get('2025-11-18T13:00:00-05:00')
|
||||
time_end = arrow.get('2025-11-18T13:29:00-05:00')
|
||||
|
||||
# Process the events
|
||||
count, last_seqnum = self.process.process(time_start, time_end)
|
||||
|
||||
# Verify that basal events were uploaded
|
||||
self.assertEqual(len(self.nightscout.uploaded_entries['treatments']), 2)
|
||||
|
||||
# First basal: from 13:12:40 to 13:17:40 = 5 minutes
|
||||
basal_1 = self.nightscout.uploaded_entries['treatments'][0]
|
||||
self.assertEqual(basal_1['duration'], 5.0)
|
||||
|
||||
# Second basal: should use events_last_time (13:22:40) not time_end (13:29:00)
|
||||
# Duration: 13:17:40 to 13:22:40 = 5 minutes
|
||||
basal_2 = self.nightscout.uploaded_entries['treatments'][1]
|
||||
expected_duration = (arrow.get('2025-11-18T13:22:40-05:00') - arrow.get('2025-11-18T13:17:40-05:00')).seconds / 60
|
||||
self.assertAlmostEqual(basal_2['duration'], expected_duration, places=2)
|
||||
self.assertEqual(basal_2['duration'], 5.0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user