Skip to content

Commit ca024cf

Browse files
committed
Make read_bg_targets only ever return data in mg/dL, not mmol/L
The OpenAPS system universally uses mg/dL for data. This helps ensure user safety by ensuring we never mix up mg/dL and mmol/L. However, the read_bg_targets code would return the values in the user-selected measurement unit. This patch makes the openaps system only ever write mg/dL to the read_bg_targets file, and then stores the user-preferred value into the file. Additionally, this change includes features to: * Convert from mg/dL to mmol/L and vs versa * Functions to support uniform display of mg/dL and mmol/L (by handling rounding appropriately, since the significant digit 6 in '5.6mmol/l' is significant, while the 6 in 100.6mg/dL is not signficant) TEST FRAMEWORK: This change also introduces a testing framework into the repository, so that we can test these functions. To get this to work, I've fixed a longstanding bug building on circleci: if you run 'sudo' to install python modules, the packages are not installed in your active virtualenv - so tests fail. I've also switched to the TravisCI infrastructure, and removed the need to sudo at all.
1 parent 957f248 commit ca024cf

14 files changed

+197
-19
lines changed

.travis.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
1+
sudo: false
22
language: python
33
python:
44
- "2.7"
55
foo:
66
- pip install -e .
77
install:
8-
- sudo python setup.py install
8+
- python setup.py install
99
script: make ci-test
10-

Makefile

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
TESTS = $(wildcard openaps/*.py openaps/*/*.py)
33

44
test:
5-
python -m unittest openaps
5+
python -m nose
66
openaps -h
77
# python -m doctest discover
88
# do the test dance
@@ -12,4 +12,3 @@ ci-test: test
1212

1313

1414
.PHONY: test
15-

circle.yml

-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
2-
dependencies:
3-
override:
4-
- sudo python setup.py install
5-
61
test:
72
override:
83
- make ci-test
9-

openaps/glucose/__init__.py

Whitespace-only changes.

openaps/glucose/convert.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class Convert(object):
2+
"""
3+
How to convert from mg/dL (World Wide format) to mmol/L (mostly UK
4+
and ex-UK colonies)
5+
6+
Please note that rounding of these values is a *view* related
7+
function, and should happen at the very last point before data is
8+
being viewed, not here. See http://physics.stackexchange.com/a/63330
9+
10+
This code *could* be used for mathematical processing of results by
11+
someone down the line, so we take pain to avoid throwing away
12+
potentially significant data by rounding.
13+
"""
14+
15+
MMOLL_CONVERT_FACTOR = 18.0
16+
17+
@classmethod
18+
def mmol_l_to_mg_dl(klass, mmol_l):
19+
return mmol_l * klass.MMOLL_CONVERT_FACTOR
20+
21+
@classmethod
22+
def mg_dl_to_mmol_l(klass, mg_dl):
23+
return mg_dl / klass.MMOLL_CONVERT_FACTOR

openaps/glucose/display.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class Display(object):
2+
"""
3+
Round Glucose values for display, so that they are consistent in
4+
all OpenAPS apps
5+
6+
Example:
7+
8+
from openaps.glucose.display import Display
9+
print(Display.display('mmol/L', 5.5))
10+
print(Display.display('mg/dL', 100))
11+
"""
12+
@classmethod
13+
def display(klass, unit, val):
14+
assert unit in ['mmol/L', 'mg/dL']
15+
16+
if unit == 'mg/dL':
17+
return int(round(val))
18+
elif unit == 'mmol/L':
19+
return round(val, 1)

openaps/vendors/medtronic.py

+26-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from openaps.uses.use import Use
66
from openaps.uses.registry import Registry
77
from openaps.configurable import Configurable
8+
from openaps.glucose.convert import Convert as GlucoseConvert
89
import decocare
910
import argparse
1011
import json
@@ -187,6 +188,31 @@ class read_clock (MedtronicTask):
187188
def main (self, args, app):
188189
return self.pump.model.read_clock( )
189190

191+
@use( )
192+
class read_bg_targets (MedtronicTask):
193+
"""
194+
Universally, OpenAPS uses blood glucose measurements in mg/dL units.
195+
196+
This code converts whatever decoding-carelink gave us to mg/dL,
197+
irrespective of the user-preferred units.
198+
199+
We then add a new field to the result, called 'user-preferred-units'.
200+
This reflects what the pump-owner prefers their unit of measurement to be.
201+
"""
202+
def main (self, args, app):
203+
bg_targets = self.pump.model.read_bg_targets( )
204+
assert bg_targets['units'] in ['mg/dL', 'mmol/L']
205+
206+
if bg_targets['units'] and bg_targets['units'] == 'mmol/L':
207+
for target in bg_targets['targets']:
208+
target['high'] = GlucoseConvert.mmol_l_to_mg_dl(target['high'])
209+
target['low'] = GlucoseConvert.mmol_l_to_mg_dl(target['low'])
210+
211+
bg_targets['user_preferred_units'] = bg_targets['units']
212+
bg_targets['units'] = 'mg/dL'
213+
214+
return bg_targets
215+
190216
class SameNameCommand (MedtronicTask):
191217
def main (self, args, app):
192218
name = self.__class__.__name__.split('.').pop( )
@@ -244,10 +270,6 @@ class resume_pump (suspend_pump):
244270
class read_battery_status (SameNameCommand):
245271
""" Check battery status. """
246272

247-
@use( )
248-
class read_bg_targets (SameNameCommand):
249-
""" Read bg targets. """
250-
251273
@use( )
252274
class read_insulin_sensitivies (SameNameCommand):
253275
""" Read insulin sensitivies. """
@@ -418,6 +440,3 @@ def get_uses (device, config):
418440
all_uses = known_uses[:] + use.get_uses(device, config)
419441
all_uses.sort(key=lambda usage: getattr(usage, 'sortOrder', usage.__name__))
420442
return all_uses
421-
422-
423-

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def readme():
1919
include_package_data = True,
2020
install_requires = [
2121
'pyserial', 'python-dateutil', 'argcomplete',
22-
'gitpython',
22+
'gitpython', 'mock', 'nose',
2323
'decocare > 0.0.16', 'dexcom_reader > 0.0.5'
2424
],
2525
dependency_links = [

tests/__init__.py

Whitespace-only changes.

tests/glucose/__init__.py

Whitespace-only changes.

tests/glucose/test_convert.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from unittest import TestCase
2+
3+
from openaps.glucose.convert import Convert
4+
5+
6+
class ConvertTestCase (TestCase):
7+
"""
8+
Test a variety of blood glucose values.
9+
Note that the Glucose converter does *not* (and should not)
10+
round values. See the glucose.round module for that.
11+
"""
12+
13+
# The List of mmol/L sugars was generated by a run of
14+
# random.uniform(0.0,35.0)
15+
MMOL_LIST = [
16+
0.10200000000000001, 1.3519999999999999, 1.6260000000000001, 3.811,
17+
4.333000000000001, 6.683, 8.024, 8.171,
18+
10.671, 11.062000000000001, 13.943999999999999, 14.902,
19+
17.683000000000003, 20.474, 24.657, 25.338, 28.008, 33.665,
20+
34.626000000000005, 34.817]
21+
22+
# This list needs to match the above MMOL_LIST - it's built mathematically
23+
# and manually, but has been checked against
24+
# http://www.diabetes.co.uk/blood-sugar-converter.html
25+
#
26+
# Note that the URL above only rounds to one digit
27+
MGDL_LIST = [
28+
1.836, 24.336, 29.268, 68.598,
29+
77.99400000000001, 120.294, 144.432, 147.07799999999997,
30+
192.07799999999997, 199.116, 250.992, 268.236,
31+
318.29400000000004, 368.532, 443.826, 456.084, 504.144, 605.97,
32+
623.268, 626.706]
33+
34+
def test_mmol_l_to_mg_dl(self):
35+
results = [Convert.mmol_l_to_mg_dl(mmol) for mmol in
36+
self.MMOL_LIST]
37+
38+
self.assertListEqual(
39+
results,
40+
self.MGDL_LIST
41+
)
42+
43+
def test_mg_dl_to_mmol_l(self):
44+
results = [Convert.mg_dl_to_mmol_l(mg_dl) for mg_dl in
45+
self.MGDL_LIST]
46+
47+
self.assertListEqual(
48+
results,
49+
self.MMOL_LIST
50+
)

tests/glucose/test_display.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from unittest import TestCase
2+
from openaps.glucose.display import Display
3+
4+
5+
class DisplayTestCase (TestCase):
6+
"""
7+
Checks that the display function rounds to the correct number
8+
of significant digits
9+
"""
10+
11+
def test_display_mmol_l(self):
12+
self.assertEqual(Display.display('mmol/L', 5.490000), 5.5)
13+
self.assertEqual(Display.display('mmol/L', 5.500001), 5.5)
14+
self.assertEqual(Display.display('mmol/L', 5.510000), 5.5)
15+
self.assertEqual(Display.display('mmol/L', 5.590000), 5.6)
16+
17+
def test_display_mg_dl(self):
18+
self.assertEqual(Display.display('mg/dL', 147.078), 147)
19+
self.assertEqual(Display.display('mg/dL', 268.236), 268)
20+
self.assertEqual(Display.display('mg/dL', 605.970), 606)
21+
self.assertEqual(Display.display('mg/dL', 623.268), 623)

tests/vendors/__init__.py

Whitespace-only changes.

tests/vendors/test_medtronic.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from unittest import TestCase
2+
from mock import Mock
3+
4+
from openaps.vendors.medtronic import read_bg_targets
5+
6+
7+
class BgTargetsTestCase(TestCase):
8+
"""Test openaps.vendor.medtronic read_bg_targets"""
9+
10+
def mg_dl_pump_response(self):
11+
return {'units': 'mg/dL', 'targets': [
12+
{'high': 200, 'low': 100},
13+
{'high': 300, 'low': 200}
14+
]}.copy()
15+
16+
def mmol_l_pump_response(self):
17+
return {'units': 'mmol/L', 'targets': [{'high': 6, 'low': 5}]}.copy()
18+
19+
class MockMethod():
20+
pass
21+
22+
class MockParent():
23+
device = 'irrelevant'
24+
25+
def test_read_bg_targets_from_mg_dl_pump(self):
26+
instance = read_bg_targets(None, BgTargetsTestCase.MockParent())
27+
28+
mock = Mock()
29+
mock.model.read_bg_targets.return_value = self.mg_dl_pump_response()
30+
instance.pump = mock
31+
32+
response = instance.main(None, None)
33+
expected_response = dict({
34+
'targets': [{'high': 200, 'low': 100}, {'high': 300, 'low': 200}],
35+
'units': 'mg/dL',
36+
'user_preferred_units': 'mg/dL',
37+
})
38+
self.assertEqual(response, expected_response)
39+
40+
def test_read_bg_targets_from_mmol_l_pump(self):
41+
instance = read_bg_targets(None, BgTargetsTestCase.MockParent())
42+
43+
mock = Mock()
44+
mock.model.read_bg_targets.return_value = self.mmol_l_pump_response()
45+
instance.pump = mock
46+
47+
expected_response = {
48+
'targets': [{'high': 108, 'low': 90}],
49+
'units': 'mg/dL',
50+
'user_preferred_units': 'mmol/L',
51+
}
52+
53+
response = instance.main(None, None)
54+
self.assertEqual(response, expected_response)

0 commit comments

Comments
 (0)