Compare commits

...
19 Commits
Author SHA1 Message Date
James Woglom f5cd7c0151 v2.2.3 2025-03-29 23:37:15 -04:00
James Woglom 13bb106924 defaults: failure time to 75 min, turn off auto restart on failure 2025-03-29 23:36:53 -04:00
James Woglom fd96b08aab stop running pipenv check 2025-01-15 19:14:20 -05:00
James Woglom 4a8da39b44 process: automatically fetch all event types for devicestatus feature 2025-01-15 18:56:36 -05:00
James Woglom 7a0f9477f2 eventparser: add todict function on all event classes 2025-01-15 18:56:08 -05:00
James Woglom 934d90e3b9 log found ProcessDeviceStatus 2025-01-02 20:05:31 -05:00
James Woglom ea84b0b9cc v2.2.2 2024-12-30 22:05:22 -05:00
James Woglom 07aaea9e1c fix tests 2024-12-30 22:05:11 -05:00
James Woglom 67b9f42f9b v2.2.1 2024-12-30 22:02:54 -05:00
James Woglom 997ab7bac8 attempt to fix batteryChargePercent 2024-12-30 22:02:36 -05:00
James Woglom b1f8263ceb v2.2.0 2024-12-13 19:47:29 -05:00
James Woglom 29d08a7f1b fix non-autoupdate 2024-12-13 19:47:01 -05:00
James Woglom c7ade49eea devicestatus: tune nightscout output to look more like Loops uploader 2024-12-13 02:09:36 -05:00
James Woglom 1f0fbd7788 less noisy logs in update profiles 2024-12-13 01:42:58 -05:00
James Woglom 4bda0a21f2 more explicit logging for last update seen 2024-12-13 01:41:16 -05:00
James Woglom d1341e09c0 devicestatus: set pump.battery.percent 2024-12-12 22:20:13 -05:00
James Woglom fb9588ff2c catch iso8601 date errors 2024-12-12 00:41:51 -05:00
James Woglom 2339b4543e v2.1.9 2024-12-12 00:36:16 -05:00
James Woglom 3f6010fafc add processdevicestatus 2024-12-12 00:36:06 -05:00
17 changed files with 793 additions and 44 deletions
+3 -3
View File
@@ -38,13 +38,13 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
@@ -55,7 +55,7 @@ jobs:
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
+19 -19
View File
@@ -28,25 +28,25 @@ jobs:
python -m pip install --upgrade pip
python -m pip install flake8 pytest pipenv
pipenv install --system
- name: Run pipenv check
run: |
# DDoS attacks in wheel and setuptools packages, not relevant
# root certificate store, not relevant
pipenv check \
--ignore 51499 \
--ignore 52495 \
--ignore 52365 \
--ignore 59956 \
--ignore 58755 \
--ignore 67895 \
--ignore 61893 \
--ignore 61601 \
--ignore 62044 \
--ignore 67599 \
--ignore 72083 \
--ignore 71064 \
--ignore 71608 \
--ignore 72236
# - name: Run pipenv check
# run: |
# # DDoS attacks in wheel and setuptools packages, not relevant
# # root certificate store, not relevant
# pipenv check \
# --ignore 51499 \
# --ignore 52495 \
# --ignore 52365 \
# --ignore 59956 \
# --ignore 58755 \
# --ignore 67895 \
# --ignore 61893 \
# --ignore 61601 \
# --ignore 62044 \
# --ignore 67599 \
# --ignore 72083 \
# --ignore 71064 \
# --ignore 71608 \
# --ignore 72236
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = tconnectsync
version = 2.1.8
version = 2.2.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
+1 -1
View File
@@ -120,7 +120,7 @@ def main(*args, **kwargs):
sys.exit(u.process(tconnect, nightscout, time_start, time_end, args.pretend, features=args.features))
else:
tconnectDevice = TandemSourceChooseDevice(secret, tconnect).choose()
added, last_event_id = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, features=args.features).process(time_start, time_end)
added, last_event_id = TandemSourceProcessTimeRange(tconnect, nightscout, tconnectDevice, pretend=args.pretend, secret=secret, features=args.features).process(time_start, time_end)
# return exit code 0 if processed events
sys.exit(0 if added>0 else 1)
@@ -24,6 +24,7 @@ class EventClass(set, Enum):
CGM_START_JOIN_STOP = {*_CGM_START, *_CGM_JOIN, *_CGM_STOP}
CGM_READING = {events.LidCgmDataGxb, events.LidCgmDataG7, events.LidCgmDataFsl2}
USER_MODE = {events.LidAaUserModeChange}
DEVICE_STATUS = {events.LidDailyBasal}
@staticmethod
def for_event(evt):
+18
View File
@@ -68,6 +68,15 @@ class {name}(BaseEvent):
def eventId(self):
return self.ID
def todict(self):
return dict(
id=self.ID,
name=self.NAME,
seqNum=self.seqNum,
eventTimestamp=str(self.eventTimestamp),
{fields_dict}
)
'''
def firstLower(text):
@@ -97,6 +106,14 @@ def build_fields(event_def):
return '\n'.join([f'{" "*4}{f}' for f in ret])
def build_fields_dict(event_def):
ret = []
for name, field in event_def["data"].items():
suffix = 'Raw' if "transform" in field and name[-3:] != 'Raw' else ''
f = f'{fieldNameFormat(name)}{suffix}=self.{fieldNameFormat(name)}{suffix},'
ret.append(f)
return '\n'.join([f'{" "*12}{f}' for f in ret])
def build_decode(event_def):
p1s = []
@@ -134,6 +151,7 @@ def build_event(event_id, event_def):
return TEMPLATE.format(
name = eventNameFormat(event_def["name"]),
fields = build_fields(event_def),
fields_dict = build_fields_dict(event_def),
build_p1 = build_decode(event_def)[0],
build_p2 = build_decode(event_def)[1],
transform_funcs = build_transform_funcs(event_def),
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -63,4 +63,11 @@ class RawEvent:
def eventTimestamp(self):
return self.timestamp
def todict(self):
return dict(
id=self.id,
name="RawEvent",
seqNum=self.seqNum,
eventTimestamp=str(self.eventTimestamp),
raw=''.join('{:02x}'.format(x) for x in self.raw),
)
+1 -1
View File
@@ -116,7 +116,7 @@ def transform_battery_charge_percent(event_def, name, name_fmt, field, tx):
out += [
'@property',
f'def batteryChargePercent(self):',
f' return (256*(self.batterychargepercentmsbRaw%2)+self.batterychargepercentlsbRaw)/512',
f' return (256*(self.batterychargepercentmsbRaw-14)+self.batterychargepercentlsbRaw)/(3*256)',
''
]
+12
View File
@@ -71,6 +71,9 @@ class NightscoutApi:
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
}, verify=self.verify)
if latest.status_code != 200:
if 'as a valid ISO-8601 date' in latest.text:
logger.warning("Nightscout last_uploaded_entry %s could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (eventType, time_start, time_end, dateFilter))
return None
raise ApiException(latest.status_code, "Nightscout last_uploaded_entry %s response: %s" % (latest.status_code, latest.text))
j = latest.json()
@@ -106,6 +109,9 @@ class NightscoutApi:
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
}, verify=self.verify)
if latest.status_code != 200:
if 'as a valid ISO-8601 date' in latest.text:
logger.warning("Nightscout last_uploaded_bg_entry could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (time_start, time_end, dateFilter))
return None
raise ApiException(latest.status_code, "Nightscout last_uploaded_bg_entry %s response: %s" % (latest.status_code, latest.text))
j = latest.json()
@@ -134,6 +140,9 @@ class NightscoutApi:
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
}, verify=self.verify)
if latest.status_code != 200:
if 'as a valid ISO-8601 date' in latest.text:
logger.warning("Nightscout activity %s could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (activityType, time_start, time_end, dateFilter))
return None
raise ApiException(latest.status_code, "Nightscout activity %s response: %s" % (latest.status_code, latest.text))
j = latest.json()
@@ -161,6 +170,9 @@ class NightscoutApi:
'api-secret': hashlib.sha1(self.secret.encode()).hexdigest()
}, verify=self.verify)
if latest.status_code != 200:
if 'as a valid ISO-8601 date' in latest.text:
logger.warning("Nightscout devicestatus could not process ISO-8601 date: start=%s end=%s dateFilter=%s" % (time_start, time_end, dateFilter))
return None
raise ApiException(latest.status_code, "Nightscout devicestatus %s response: %s" % (latest.status_code, latest.text))
j = latest.json()
+4 -2
View File
@@ -195,14 +195,16 @@ class NightscoutEntry:
}
@staticmethod
def devicestatus(created_at, batteryVoltage, batteryString, pump_event_id=""):
def devicestatus(created_at, batteryVoltage, batteryPercent, pump_event_id=""):
return {
"device": ENTERED_BY,
"created_at": created_at,
"pump": {
"clock": created_at,
"battery": {
"voltage": float(batteryVoltage),
"string": batteryString
"percent": int(batteryPercent) if batteryPercent else None,
"status": "%.0f%s" % (batteryPercent, '%')
},
},
"pump_event_id": pump_event_id
+2 -2
View File
@@ -70,8 +70,8 @@ AUTOUPDATE_MAX_SLEEP_SECONDS = get_number('AUTOUPDATE_MAX_SLEEP_SECONDS', '1500'
AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS = get_number('AUTOUPDATE_UNEXPECTED_NO_INDEX_SLEEP_SECONDS', '60') # 1 minute
AUTOUPDATE_USE_FIXED_SLEEP = get_bool('AUTOUPDATE_USE_FIXED_SLEEP', 'false')
AUTOUPDATE_NO_DATA_FAILURE_MINUTES = get_number('AUTOUPDATE_NO_DATA_FAILURE_MINUTES', '180') # 3 hours
AUTOUPDATE_FAILURE_MINUTES = get_number('AUTOUPDATE_FAILURE_MINUTES', '15') # 15 minutes
AUTOUPDATE_RESTART_ON_FAILURE = get_bool('AUTOUPDATE_RESTART_ON_FAILURE', 'true')
AUTOUPDATE_FAILURE_MINUTES = get_number('AUTOUPDATE_FAILURE_MINUTES', '75') # 75 minutes
AUTOUPDATE_RESTART_ON_FAILURE = get_bool('AUTOUPDATE_RESTART_ON_FAILURE', 'false')
AUTOUPDATE_MAX_LOOP_INVOCATIONS = get_number('AUTOUPDATE_MAX_LOOP_INVOCATIONS', '-1')
NIGHTSCOUT_PROFILE_UPLOAD_MODE = get_one_of('NIGHTSCOUT_PROFILE_UPLOAD_MODE', 'add', ['add', 'replace'])
+7 -2
View File
@@ -69,7 +69,12 @@ class TandemSourceAutoupdate:
self.last_attempt_time = now
self.time_diffs_between_attempts = []
else:
logger.info('No new reported tandemsource data. (cur_max_date: %s)' % cur_max_date_with_events)
logger.info('No new reported tandemsource data. cur_max_date: %s (%dm ago) last_event_time: %s (%dm ago)' % (
arrow.get(cur_max_date_with_events) if cur_max_date_with_events else None,
(now - cur_max_date_with_events)//60 if cur_max_date_with_events else None,
arrow.get(self.last_event_time) if self.last_event_time else None,
(now - self.last_event_time)//60 if self.last_event_time else None
))
# If we haven't seen the pump event index update in AUTOUPDATE_NO_DATA_FAILURE_MINUTES,
# then trigger an error and potentially restart.
@@ -93,7 +98,7 @@ class TandemSourceAutoupdate:
# above no indexes warning.
elif self.last_successful_process_time_range and (now - self.last_successful_process_time_range) >= 60 * self.secret.AUTOUPDATE_FAILURE_MINUTES:
logger.error(AutoupdateNoNewDataDetectedError(
"%s: No new data has been detected via the API for %d minutes. " % (datetime.datetime.now(), (now - self.last_successful_process_time_range)//60) +
"%s: No new data has been detected via the API for %d minutes (last: %s). " % (datetime.datetime.now(), (now - self.last_successful_process_time_range)//60, self.last_successful_process_time_range) +
"tconnectsync might not be functioning properly."))
if self.secret.AUTOUPDATE_RESTART_ON_FAILURE:
+7 -3
View File
@@ -1,7 +1,7 @@
import logging
import collections
from ...features import DEFAULT_FEATURES
from ...features import DEVICE_STATUS, DEFAULT_FEATURES
from ...eventparser import events as eventtypes
from ...domain.tandemsource.event_class import EventClass
from .process_basal import ProcessBasal
@@ -13,6 +13,7 @@ from .process_cartridge import ProcessCartridge
from .process_cgm_alert import ProcessCGMAlert
from .process_cgm_start_join_stop import ProcessCGMStartJoinStop
from .process_cgm_reading import ProcessCGMReading
from .process_device_status import ProcessDeviceStatus
from .process_user_mode import ProcessUserMode
from .update_profiles import UpdateProfiles
@@ -39,6 +40,7 @@ class ProcessTimeRange:
EventClass.CGM_START_JOIN_STOP.name: ProcessCGMStartJoinStop,
EventClass.CGM_READING.name: ProcessCGMReading,
EventClass.USER_MODE.name: ProcessUserMode,
EventClass.DEVICE_STATUS.name: ProcessDeviceStatus
}
updater_classes = [
@@ -46,8 +48,10 @@ class ProcessTimeRange:
]
def process(self, time_start, time_end):
logger.info(f"ProcessTimeRange time_start={time_start} time_end={time_end} tconnect_device_id={self.tconnect_device_id} features={self.features}")
events = self.tconnect.tandemsource.pump_events(self.tconnect_device_id, time_start, time_end, fetch_all_event_types=self.secret.FETCH_ALL_EVENT_TYPES)
fetch_all_event_types = self.secret.FETCH_ALL_EVENT_TYPES or DEVICE_STATUS in self.features
logger.info(f"ProcessTimeRange time_start={time_start} time_end={time_end} tconnect_device_id={self.tconnect_device_id} features={self.features} fetch_all_event_types={fetch_all_event_types}")
events = self.tconnect.tandemsource.pump_events(self.tconnect_device_id, time_start, time_end, fetch_all_event_types=fetch_all_event_types)
events_first_time = None
events_last_time = None
@@ -50,6 +50,7 @@ class ProcessDeviceStatus:
logger.info("ProcessDeviceStatus: No last_daily_basal_event found for add (time range: %s - %s)" % (time_start, time_end))
return []
logger.info("ProcessDeviceStatus: last_daily_basal_event=%s" % (last_daily_basal_event))
ns_entries = []
ns_entries.append(self.daily_basal_to_nsentry(last_daily_basal_event))
@@ -59,7 +60,7 @@ class ProcessDeviceStatus:
return NightscoutEntry.devicestatus(
created_at=event.eventTimestamp.format(),
batteryVoltage=(float(event.batterylipomillivolts or 0)/1000),
batteryString="%.0f%s" % (100*event.batteryChargePercent, '%'),
batteryPercent=int(100*event.batteryChargePercent),
pump_event_id = "%s" % event.seqNum
)
@@ -49,7 +49,7 @@ class UpdateProfiles:
logger.info("Current pump settings: %s" % pump_settings)
ns_profile_obj = self.nightscout.current_profile()
logger.info("Current Nightscout profile: %s" % ns_profile_obj)
logger.debug("Current Nightscout profile: %s" % ns_profile_obj)
if ns_profile_obj is None:
ns_profile_obj = {}
@@ -95,7 +95,7 @@ class UpdateProfiles:
ns = ns_profile_obj.get('store', {})
logger.info("compare_profiles profile names: device: %s ns: %s", device.keys(), ns.keys())
logger.debug("compare_profiles profile names: device: %s ns: %s", device.keys(), ns.keys())
new_ns_profile = copy.deepcopy(ns_profile_obj)
if not 'store' in new_ns_profile:
@@ -113,12 +113,12 @@ class UpdateProfiles:
existent_profiles_in_ns = set(device.keys()) & set(ns.keys())
for profile_name in existent_profiles_in_ns:
logger.debug("Checking for differences for %s profile between pump and nightscout", profile_name)
#logger.debug("Checking for differences for %s profile between pump and nightscout", profile_name)
pump_configured_profile = device[profile_name]
ns_translated_profile = NightscoutEntry.tandemsource_profile_store(pump_configured_profile, pump_settings)
ns_configured_profile = ns[profile_name]
logger.debug("Comparing %s profile from pump: %s to nightscout: %s", profile_name, ns_translated_profile, ns_configured_profile)
#logger.debug("Comparing %s profile from pump: %s to nightscout: %s", profile_name, ns_translated_profile, ns_configured_profile)
if self.nightscout_profiles_identical(ns_configured_profile, ns_translated_profile):
logger.info("Profile %s identical between pump and nightscout", profile_name)
continue
@@ -37,8 +37,10 @@ class TestProcessDeviceStatus(unittest.TestCase):
'created_at': '2024-12-03 23:40:23-05:00',
'device': 'Pump (tconnectsync)',
'pump': {
'clock': '2024-12-03 23:40:23-05:00',
'battery': {
'string': '48%',
'status': '32%',
'percent': 32,
'voltage': 14.08
}
},
@@ -89,8 +91,10 @@ class TestProcessDeviceStatus(unittest.TestCase):
'created_at': '2024-12-04 02:30:23-05:00',
'device': 'Pump (tconnectsync)',
'pump': {
'clock': '2024-12-04 02:30:23-05:00',
'battery': {
'string': '47%',
'status': '31%',
'percent': 31,
'voltage': 13.824
}
},
@@ -131,8 +135,10 @@ class TestProcessDeviceStatus(unittest.TestCase):
'created_at': '2024-12-04 05:50:23-05:00',
'device': 'Pump (tconnectsync)',
'pump': {
'clock': '2024-12-04 05:50:23-05:00',
'battery': {
'string': '46%',
'status': '30%',
'percent': 30,
'voltage': 13.568
}
},