From e72b02d84e7f67cdf6107862ad580e951a5bbda1 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 21 Feb 2020 10:45:40 +0100 Subject: format code with black --- README | 150 ----------------- README.md | 150 +++++++++++++++++ glucometerutils/common.py | 87 ++++++---- glucometerutils/drivers/accuchek_reports.py | 73 ++++---- glucometerutils/drivers/contourusb.py | 39 ++--- glucometerutils/drivers/fsinsulinx.py | 55 +++--- glucometerutils/drivers/fslibre.py | 184 ++++++++++----------- glucometerutils/drivers/fsoptium.py | 134 ++++++++------- glucometerutils/drivers/fsprecisionneo.py | 61 ++++--- glucometerutils/drivers/otultra2.py | 112 +++++++------ glucometerutils/drivers/otultraeasy.py | 149 +++++++++-------- glucometerutils/drivers/otverio2015.py | 123 +++++++------- glucometerutils/drivers/otverioiq.py | 104 ++++++------ glucometerutils/drivers/sdcodefree.py | 108 ++++++------ glucometerutils/drivers/td4277.py | 129 +++++++-------- glucometerutils/exceptions.py | 17 +- glucometerutils/glucometer.py | 158 +++++++++++------- glucometerutils/support/construct_extras.py | 2 + glucometerutils/support/contourusb.py | 163 +++++++++--------- glucometerutils/support/driver_base.py | 1 - glucometerutils/support/freestyle.py | 147 ++++++++-------- glucometerutils/support/hiddevice.py | 26 +-- glucometerutils/support/lifescan.py | 23 +-- .../support/lifescan_binary_protocol.py | 45 ++--- glucometerutils/support/serial.py | 17 +- reversing_tools/abbott/extract_freestyle.py | 109 +++++++----- reversing_tools/abbott/freestyle_hid_console.py | 42 +++-- test/test_common.py | 131 ++++++++------- test/test_construct_extras.py | 50 +++--- test/test_contourusb.py | 65 ++++---- test/test_freestyle.py | 9 +- test/test_fsoptium.py | 25 +-- test/test_lifescan.py | 11 +- test/test_otultra2.py | 30 ++-- test/test_otultraeasy.py | 13 +- test/test_td4277.py | 17 +- 36 files changed, 1447 insertions(+), 1312 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 0f97db2..0000000 --- a/README +++ /dev/null @@ -1,150 +0,0 @@ -

-build status -GitHub -Code style: black -

- -# Glucometer Utilities - -This repository includes a command line utility to interact with a number of -blood sugar meters (glucometer) models from various manufacturers. - -While support varies by device, the actions that may be available are as -follows: - - * `info` shows the model, serial number, date and time, and configured glucose - unit of the device. - * `dump` export the recorded blood sugar or β-ketone readings from the device - in comma-separated values format. - * `datetime` reads or updates the date and time of the device clock. - * `zero` deletes all the recorded readings (only implemented for few devices). - -## Example Usage - -Most of the drivers require optional dependencies, and those are listed in the -table below. If you do not want to install the dependencies manually, you should -be able to set this up using `virtualenv` and `pip`: - -```shell -$ python3 -m venv $(pwd)/glucometerutils-venv -$ . glucometerutils-venv/bin/activate -(glucometerutils-venv) $ DRIVER=myglucometer-driver # see table below -(glucometerutils-venv) $ pip install "git+https://github.com/Flameeyes/glucometerutils.git#egg=glucometerutils[${DRIVER}]" -(glucometerutils-venv) $ glucometer --driver ${DRIVER} help -``` - -## Supported devices - -Please see the following table for the driver for each device that is known and -supported. - -| Manufacturer | Model Name | Driver | Dependencies | -| --- | --- | --- | --- | -| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | -| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | -| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | -| LifeScan | OneTouch Verio IQ | `otverioiq` | [construct] [pyserial] | -| LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | -| LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | -| LifeScan | OneTouch Select Plus Flex¹ | `otverio2015` | [construct] [python-scsi] | -| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | -| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | -| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | -| Roche | Accu-Chek Mobile | `accuchek_reports` | | -| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | -| TaiDoc | TD-4277 | `td4277` | [construct] [pyserial]² [hidapi] | -| GlucoRx | Nexus | `td4277` | [construct] [pyserial]² [hidapi] | -| Menarini | GlucoMen Nexus | `td4277` | [construct] [pyserial]² [hidapi] | -| Aktivmed | GlucoCheck XL | `td4277` | [construct] [pyserial]² [hidapi] | -| Ascensia | ContourUSB | `contourusb` | [construct] [hidapi]‡ | - -† Untested. - -‡ Optional dependency on Linux; required on other operating systems. - -¹ USB only, bluetooth not supported. - -² Requires a version of pyserial supporting CP2110 bridges. See [this pyserial -pull request](https://github.com/pyserial/pyserial/pull/411). - -To identify the supported features for each of the driver, query the `help` -action: - - glucometer.py --driver fslibre help - -If you have knowledge of a protocol of a glucometer you would have supported, -please provide a reference, possibly by writing a specification and contribute -it to https://protocols.glucometers.tech/ . - -[construct]: https://construct.readthedocs.io/en/latest/ -[pyserial]: https://pythonhosted.org/pyserial/ -[python-scsi]: https://github.com/rosjat/python-scsi -[hidapi]: https://pypi.python.org/pypi/hidapi - -## Dump format - -The `dump` action by default will output CSV-compatible format, with the -following fields: - - * date and time; - * meter reading value; - * before/after meal information, if known; - * comment provided with the reading, if any. - -Meal and comment information is provided by the meters supporting the -information. In the future, meal information could be guessed based on the time -of the reading. - -The unit format used by the dump by default matches what the meter reports as -its display unit, which might differ from the one used by the meter for internal -representation and wire protocol. You can override the display unit with -`--unit`. - -## Development - -The tool is being written keeping in mind that different glucometers, -even if they are all from the same manufacturer, will use different -protocols. - -If you want to contribute code, please note that the target language -is Python 3.6, and that the style to follow is for the most part PEP8 -compatible. - -To set up your development environment follow these guidelines: - -```shell -$ git clone https://github.com/Flameeyes/glucometerutils.git -$ cd glucometerutils -$ python3 -m venv --python=python3.6 -$ . venv/bin/activate -$ pip install -e .[dev] -$ # If you want to work on a specific driver specify this after dev e.g. -$ # pip install -e .[dev,myglucometer-driver] # see table above -$ pre-commit install -``` - -## License - -Copyright © 2013-2020 The glucometerutils Authors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f97db2 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +

+build status +GitHub +Code style: black +

+ +# Glucometer Utilities + +This repository includes a command line utility to interact with a number of +blood sugar meters (glucometer) models from various manufacturers. + +While support varies by device, the actions that may be available are as +follows: + + * `info` shows the model, serial number, date and time, and configured glucose + unit of the device. + * `dump` export the recorded blood sugar or β-ketone readings from the device + in comma-separated values format. + * `datetime` reads or updates the date and time of the device clock. + * `zero` deletes all the recorded readings (only implemented for few devices). + +## Example Usage + +Most of the drivers require optional dependencies, and those are listed in the +table below. If you do not want to install the dependencies manually, you should +be able to set this up using `virtualenv` and `pip`: + +```shell +$ python3 -m venv $(pwd)/glucometerutils-venv +$ . glucometerutils-venv/bin/activate +(glucometerutils-venv) $ DRIVER=myglucometer-driver # see table below +(glucometerutils-venv) $ pip install "git+https://github.com/Flameeyes/glucometerutils.git#egg=glucometerutils[${DRIVER}]" +(glucometerutils-venv) $ glucometer --driver ${DRIVER} help +``` + +## Supported devices + +Please see the following table for the driver for each device that is known and +supported. + +| Manufacturer | Model Name | Driver | Dependencies | +| --- | --- | --- | --- | +| LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | +| LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | +| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | +| LifeScan | OneTouch Verio IQ | `otverioiq` | [construct] [pyserial] | +| LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | +| LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | +| LifeScan | OneTouch Select Plus Flex¹ | `otverio2015` | [construct] [python-scsi] | +| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Libre | `fslibre` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | +| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [construct] [hidapi]‡ | +| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [construct] [hidapi]‡ | +| Roche | Accu-Chek Mobile | `accuchek_reports` | | +| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | +| TaiDoc | TD-4277 | `td4277` | [construct] [pyserial]² [hidapi] | +| GlucoRx | Nexus | `td4277` | [construct] [pyserial]² [hidapi] | +| Menarini | GlucoMen Nexus | `td4277` | [construct] [pyserial]² [hidapi] | +| Aktivmed | GlucoCheck XL | `td4277` | [construct] [pyserial]² [hidapi] | +| Ascensia | ContourUSB | `contourusb` | [construct] [hidapi]‡ | + +† Untested. + +‡ Optional dependency on Linux; required on other operating systems. + +¹ USB only, bluetooth not supported. + +² Requires a version of pyserial supporting CP2110 bridges. See [this pyserial +pull request](https://github.com/pyserial/pyserial/pull/411). + +To identify the supported features for each of the driver, query the `help` +action: + + glucometer.py --driver fslibre help + +If you have knowledge of a protocol of a glucometer you would have supported, +please provide a reference, possibly by writing a specification and contribute +it to https://protocols.glucometers.tech/ . + +[construct]: https://construct.readthedocs.io/en/latest/ +[pyserial]: https://pythonhosted.org/pyserial/ +[python-scsi]: https://github.com/rosjat/python-scsi +[hidapi]: https://pypi.python.org/pypi/hidapi + +## Dump format + +The `dump` action by default will output CSV-compatible format, with the +following fields: + + * date and time; + * meter reading value; + * before/after meal information, if known; + * comment provided with the reading, if any. + +Meal and comment information is provided by the meters supporting the +information. In the future, meal information could be guessed based on the time +of the reading. + +The unit format used by the dump by default matches what the meter reports as +its display unit, which might differ from the one used by the meter for internal +representation and wire protocol. You can override the display unit with +`--unit`. + +## Development + +The tool is being written keeping in mind that different glucometers, +even if they are all from the same manufacturer, will use different +protocols. + +If you want to contribute code, please note that the target language +is Python 3.6, and that the style to follow is for the most part PEP8 +compatible. + +To set up your development environment follow these guidelines: + +```shell +$ git clone https://github.com/Flameeyes/glucometerutils.git +$ cd glucometerutils +$ python3 -m venv --python=python3.6 +$ . venv/bin/activate +$ pip install -e .[dev] +$ # If you want to work on a specific driver specify this after dev e.g. +$ # pip install -e .[dev,myglucometer-driver] # see table above +$ pre-commit install +``` + +## License + +Copyright © 2013-2020 The glucometerutils Authors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/glucometerutils/common.py b/glucometerutils/common.py index e7be7ec..6576ebd 100644 --- a/glucometerutils/common.py +++ b/glucometerutils/common.py @@ -10,21 +10,24 @@ from typing import Optional, Sequence import attr + class Unit(enum.Enum): - MG_DL = 'mg/dL' - MMOL_L = 'mmol/L' + MG_DL = "mg/dL" + MMOL_L = "mmol/L" + # Constants for meal information class Meal(enum.Enum): - NONE = '' - BEFORE = 'Before Meal' - AFTER = 'After Meal' + NONE = "" + BEFORE = "Before Meal" + AFTER = "After Meal" + # Constants for measure method class MeasurementMethod(enum.Enum): - BLOOD_SAMPLE = 'blood sample' - CGM = 'CGM' # Continuous Glucose Monitoring - TIME = 'time' + BLOOD_SAMPLE = "blood sample" + CGM = "CGM" # Continuous Glucose Monitoring + TIME = "time" def convert_glucose_unit(value, from_unit, to_unit): @@ -50,19 +53,19 @@ def convert_glucose_unit(value, from_unit, to_unit): return round(value * 18.0, 0) + @attr.s class GlucoseReading: timestamp = attr.ib(type=datetime.datetime) value = attr.ib(type=float) - meal = attr.ib( - default=Meal.NONE, validator=attr.validators.in_(Meal), - type=Meal) - comment = attr.ib(default='', type=str) + meal = attr.ib(default=Meal.NONE, validator=attr.validators.in_(Meal), type=Meal) + comment = attr.ib(default="", type=str) measure_method = attr.ib( default=MeasurementMethod.BLOOD_SAMPLE, validator=attr.validators.in_(MeasurementMethod), - type=MeasurementMethod) + type=MeasurementMethod, + ) extra_data = attr.ib(factory=dict) def get_value_as(self, to_unit): @@ -78,15 +81,20 @@ class GlucoseReading: # type: (Unit) -> str """Returns the reading as a formatted comma-separated value string.""" return '"%s","%.2f","%s","%s","%s"' % ( - self.timestamp, self.get_value_as(unit), self.meal.value, - self.measure_method.value, self.comment) + self.timestamp, + self.get_value_as(unit), + self.meal.value, + self.measure_method.value, + self.comment, + ) + @attr.s class KetoneReading: timestamp = attr.ib(type=datetime.datetime) value = attr.ib(type=float) - comment = attr.ib(default='', type=str) + comment = attr.ib(default="", type=str) extra_data = attr.ib(factory=dict) def as_csv(self, unit): @@ -94,23 +102,28 @@ class KetoneReading: del unit # Unused for Ketone readings. return '"%s","%.2f","%s","%s"' % ( - self.timestamp, self.value, MeasurementMethod.BLOOD_SAMPLE.value, - self.comment) + self.timestamp, + self.value, + MeasurementMethod.BLOOD_SAMPLE.value, + self.comment, + ) + @attr.s class TimeAdjustment: timestamp = attr.ib() # type: datetime.datetime old_timestamp = attr.ib() # type: datetime.datetime measure_method = attr.ib( - default=MeasurementMethod.TIME, - validator=attr.validators.in_( - MeasurementMethod)) # type: MeasurementMethod + default=MeasurementMethod.TIME, validator=attr.validators.in_(MeasurementMethod) + ) # type: MeasurementMethod extra_data = attr.ib(factory=dict) def as_csv(self, unit): del unit return '"%s","","%s","%s"' % ( - self.timestamp, self.measure_method.value, self.old_timestamp + self.timestamp, + self.measure_method.value, + self.old_timestamp, ) @@ -126,32 +139,38 @@ class MeterInfo: the device. It can include hardware and software version. native_unit: One of the Unit values to identify the meter native unit. """ + model = attr.ib(type=str) - serial_number = attr.ib(default='N/A', type=str) + serial_number = attr.ib(default="N/A", type=str) version_info = attr.ib(default=(), type=Sequence[str]) native_unit = attr.ib( - default=Unit.MG_DL, validator=attr.validators.in_(Unit), - type=Unit) + default=Unit.MG_DL, validator=attr.validators.in_(Unit), type=Unit + ) patient_name = attr.ib(default=None, type=Optional[str]) def __str__(self): - version_information_string = 'N/A' + version_information_string = "N/A" if self.version_info: - version_information_string = '\n '.join( - self.version_info).strip() + version_information_string = "\n ".join(self.version_info).strip() - base_output = textwrap.dedent("""\ + base_output = textwrap.dedent( + """\ {model} Serial Number: {serial_number} Version Information: {version_information_string} Native Unit: {native_unit} - """).format(model=self.model, serial_number=self.serial_number, - version_information_string=version_information_string, - native_unit=self.native_unit.value) + """ + ).format( + model=self.model, + serial_number=self.serial_number, + version_information_string=version_information_string, + native_unit=self.native_unit.value, + ) if self.patient_name != None: - base_output += 'Patient Name: {patient_name}\n'.format( - patient_name=self.patient_name) + base_output += "Patient Name: {patient_name}\n".format( + patient_name=self.patient_name + ) return base_output diff --git a/glucometerutils/drivers/accuchek_reports.py b/glucometerutils/drivers/accuchek_reports.py index 06b69bc..c4d7527 100644 --- a/glucometerutils/drivers/accuchek_reports.py +++ b/glucometerutils/drivers/accuchek_reports.py @@ -19,45 +19,46 @@ import datetime import glob import os -from glucometerutils import common -from glucometerutils import exceptions +from glucometerutils import common, exceptions from glucometerutils.support import driver_base _UNIT_MAP = { - 'mmol/l': common.Unit.MMOL_L, - 'mg/dl': common.Unit.MG_DL, + "mmol/l": common.Unit.MMOL_L, + "mg/dl": common.Unit.MG_DL, } -_DATE_CSV_KEY = 'Date' -_TIME_CSV_KEY = 'Time' -_RESULT_CSV_KEY = 'Result' -_UNIT_CSV_KEY = 'Unit' -_TEMPWARNING_CSV_KEY = 'Temperature warning' # ignored -_OUTRANGE_CSV_KEY = 'Out of target range' # ignored -_OTHER_CSV_KEY = 'Other' # ignored -_BEFORE_MEAL_CSV_KEY = 'Before meal' -_AFTER_MEAL_CSV_KEY = 'After meal' +_DATE_CSV_KEY = "Date" +_TIME_CSV_KEY = "Time" +_RESULT_CSV_KEY = "Result" +_UNIT_CSV_KEY = "Unit" +_TEMPWARNING_CSV_KEY = "Temperature warning" # ignored +_OUTRANGE_CSV_KEY = "Out of target range" # ignored +_OTHER_CSV_KEY = "Other" # ignored +_BEFORE_MEAL_CSV_KEY = "Before meal" +_AFTER_MEAL_CSV_KEY = "After meal" # Control test has extra whitespace which is not ignored. -_CONTROL_CSV_KEY = 'Control test' + ' '*197 +_CONTROL_CSV_KEY = "Control test" + " " * 197 -_DATE_FORMAT = '%d.%m.%Y' -_TIME_FORMAT = '%H:%M' +_DATE_FORMAT = "%d.%m.%Y" +_TIME_FORMAT = "%H:%M" -_DATETIME_FORMAT = ' '.join((_DATE_FORMAT, _TIME_FORMAT)) +_DATETIME_FORMAT = " ".join((_DATE_FORMAT, _TIME_FORMAT)) class Device(driver_base.GlucometerDriver): def __init__(self, device): if not device or not os.path.isdir(device): raise exceptions.CommandLineError( - '--device parameter is required, should point to mount path ' - 'for the meter.') + "--device parameter is required, should point to mount path " + "for the meter." + ) - reports_path = os.path.join(device, '*', 'Reports', '*.csv') + reports_path = os.path.join(device, "*", "Reports", "*.csv") report_files = glob.glob(reports_path) if not report_files: raise exceptions.ConnectionFailed( - 'No report file found in path "%s".' % reports_path) + 'No report file found in path "%s".' % reports_path + ) self.report_file = report_files[0] @@ -68,35 +69,32 @@ class Device(driver_base.GlucometerDriver): next(self.report) return csv.DictReader( - self.report, - delimiter=';', - skipinitialspace=True, - quoting=csv.QUOTE_NONE) + self.report, delimiter=";", skipinitialspace=True, quoting=csv.QUOTE_NONE + ) def connect(self): - self.report = open( - self.report_file, 'r', newline='\r\n', encoding='utf-8') + self.report = open(self.report_file, "r", newline="\r\n", encoding="utf-8") def disconnect(self): self.report.close() def get_meter_info(self): return common.MeterInfo( - '%s glucometer' % self.get_model(), + "%s glucometer" % self.get_model(), serial_number=self.get_serial_number(), - native_unit=self.get_glucose_unit()) + native_unit=self.get_glucose_unit(), + ) def get_model(self): # $device/MODEL/Reports/*.csv - return os.path.basename( - os.path.dirname(os.path.dirname(self.report_file))) + return os.path.basename(os.path.dirname(os.path.dirname(self.report_file))) def get_serial_number(self): self.report.seek(0) # ignore the first line. next(self.report) # The second line of the CSV is serial-no;report-date;report-time;;;;;;; - return next(self.report).split(';')[0] + return next(self.report).split(";")[0] def get_glucose_unit(self): # Get the first record available and parse that. @@ -115,13 +113,12 @@ class Device(driver_base.GlucometerDriver): def _extract_datetime(self, record): # pylint: disable=no-self-use # Date and time are in separate column, but we want to parse them # together. - date_and_time = ' '.join((record[_DATE_CSV_KEY], record[_TIME_CSV_KEY])) + date_and_time = " ".join((record[_DATE_CSV_KEY], record[_TIME_CSV_KEY])) return datetime.datetime.strptime(date_and_time, _DATETIME_FORMAT) def _extract_meal(self, record): # pylint: disable=no-self-use if record[_AFTER_MEAL_CSV_KEY] and record[_BEFORE_MEAL_CSV_KEY]: - raise exceptions.InvalidResponse( - 'Reading cannot be before and after meal.') + raise exceptions.InvalidResponse("Reading cannot be before and after meal.") elif record[_AFTER_MEAL_CSV_KEY]: return common.Meal.AFTER elif record[_BEFORE_MEAL_CSV_KEY]: @@ -139,5 +136,7 @@ class Device(driver_base.GlucometerDriver): common.convert_glucose_unit( float(record[_RESULT_CSV_KEY]), _UNIT_MAP[record[_UNIT_CSV_KEY]], - common.Unit.MG_DL), - meal=self._extract_meal(record)) + common.Unit.MG_DL, + ), + meal=self._extract_meal(record), + ) diff --git a/glucometerutils/drivers/contourusb.py b/glucometerutils/drivers/contourusb.py index 397eb4f..5c9ed11 100644 --- a/glucometerutils/drivers/contourusb.py +++ b/glucometerutils/drivers/contourusb.py @@ -24,43 +24,44 @@ from glucometerutils import common from glucometerutils.support import contourusb, driver_base -def _extract_timestamp(parsed_record, prefix=''): +def _extract_timestamp(parsed_record, prefix=""): """Extract the timestamp from a parsed record. This leverages the fact that all the reading records have the same base structure. """ - datetime_str = parsed_record['datetime'] + datetime_str = parsed_record["datetime"] return datetime.datetime( - int(datetime_str[0:4]), #year - int(datetime_str[4:6]), #month - int(datetime_str[6:8]), #day - int(datetime_str[8:10]), #hour - int(datetime_str[10:12]), #minute - 0) + int(datetime_str[0:4]), # year + int(datetime_str[4:6]), # month + int(datetime_str[6:8]), # day + int(datetime_str[8:10]), # hour + int(datetime_str[10:12]), # minute + 0, + ) class Device(contourusb.ContourHidDevice, driver_base.GlucometerDriver): """Glucometer driver for FreeStyle Libre devices.""" - USB_VENDOR_ID = 0x1a79 # type: int # Bayer Health Care LLC Contour + USB_VENDOR_ID = 0x1A79 # type: int # Bayer Health Care LLC Contour USB_PRODUCT_ID = 0x6002 # type: int def get_meter_info(self): self._get_info_record() return common.MeterInfo( - 'Contour USB', + "Contour USB", serial_number=self._get_serial_number(), - version_info=( - 'Meter versions: ' + self._get_version(),), - native_unit= self.get_glucose_unit()) + version_info=("Meter versions: " + self._get_version(),), + native_unit=self.get_glucose_unit(), + ) def get_glucose_unit(self): # pylint: disable=no-self-use - if self._get_glucose_unit() == '0': + if self._get_glucose_unit() == "0": return common.Unit.MG_DL else: return common.Unit.MMOL_L - + def get_readings(self): """ Get reading dump from download data mode(all readings stored) @@ -69,10 +70,10 @@ class Device(contourusb.ContourHidDevice, driver_base.GlucometerDriver): for parsed_record in self._get_multirecord(): yield common.GlucoseReading( _extract_timestamp(parsed_record), - int(parsed_record['value']), - comment=parsed_record['markers'], - measure_method=common.MeasurementMethod.BLOOD_SAMPLE - ) + int(parsed_record["value"]), + comment=parsed_record["markers"], + measure_method=common.MeasurementMethod.BLOOD_SAMPLE, + ) def get_serial_number(self): raise NotImplementedError diff --git a/glucometerutils/drivers/fsinsulinx.py b/glucometerutils/drivers/fsinsulinx.py index f3cf043..5465b3a 100644 --- a/glucometerutils/drivers/fsinsulinx.py +++ b/glucometerutils/drivers/fsinsulinx.py @@ -22,20 +22,30 @@ import datetime from glucometerutils import common from glucometerutils.support import freestyle - # The type is a string because it precedes the parsing of the object. -_TYPE_GLUCOSE_READING = '0' - -_InsulinxReading = collections.namedtuple('_InsulinxReading', ( - 'type', # 0 = blood glucose - 'id', - 'month', 'day', 'year', # year is two-digits - 'hour', 'minute', - 'unknown1', 'unknown2', 'unknown3', - 'unknown4', 'unknown5', 'unknown6', - 'value', - 'unknown7', 'unknown8', -)) +_TYPE_GLUCOSE_READING = "0" + +_InsulinxReading = collections.namedtuple( + "_InsulinxReading", + ( + "type", # 0 = blood glucose + "id", + "month", + "day", + "year", # year is two-digits + "hour", + "minute", + "unknown1", + "unknown2", + "unknown3", + "unknown4", + "unknown5", + "unknown6", + "value", + "unknown7", + "unknown8", + ), +) class Device(freestyle.FreeStyleHidDevice): @@ -46,11 +56,11 @@ class Device(freestyle.FreeStyleHidDevice): def get_meter_info(self): """Return the device information in structured form.""" return common.MeterInfo( - 'FreeStyle InsuLinx', + "FreeStyle InsuLinx", serial_number=self.get_serial_number(), - version_info=( - 'Software version: ' + self._get_version(),), - native_unit=self.get_glucose_unit()) + version_info=("Software version: " + self._get_version(),), + native_unit=self.get_glucose_unit(), + ) def get_glucose_unit(self): # pylint: disable=no-self-use """Returns the glucose unit of the device.""" @@ -58,7 +68,7 @@ class Device(freestyle.FreeStyleHidDevice): def get_readings(self): """Iterate through the reading records in the device.""" - for record in self._get_multirecord(b'$result?'): + for record in self._get_multirecord(b"$result?"): if not record or record[0] != _TYPE_GLUCOSE_READING: continue @@ -67,11 +77,14 @@ class Device(freestyle.FreeStyleHidDevice): raw_reading = _InsulinxReading._make([int(v) for v in record]) timestamp = datetime.datetime( - raw_reading.year + 2000, raw_reading.month, raw_reading.day, - raw_reading.hour, raw_reading.minute) + raw_reading.year + 2000, + raw_reading.month, + raw_reading.day, + raw_reading.hour, + raw_reading.minute, + ) yield common.GlucoseReading(timestamp, raw_reading.value) def zero_log(self): raise NotImplementedError - diff --git a/glucometerutils/drivers/fslibre.py b/glucometerutils/drivers/fslibre.py index f1ac525..29e821a 100644 --- a/glucometerutils/drivers/fslibre.py +++ b/glucometerutils/drivers/fslibre.py @@ -27,52 +27,47 @@ from glucometerutils.support import freestyle # Fields of the records returned by both $history and $arresult? # Tuple of pairs of idx and field name _BASE_ENTRY_MAP = ( - (0, 'device_id'), - (1, 'type'), - (2, 'month'), - (3, 'day'), - (4, 'year'), # 2-digits - (5, 'hour'), - (6, 'minute'), - (7, 'second'), + (0, "device_id"), + (1, "type"), + (2, "month"), + (3, "day"), + (4, "year"), # 2-digits + (5, "hour"), + (6, "minute"), + (7, "second"), ) # Fields of the records returned by $history? -_HISTORY_ENTRY_MAP = _BASE_ENTRY_MAP + ( - (13, 'value'), - (15, 'errors'), -) +_HISTORY_ENTRY_MAP = _BASE_ENTRY_MAP + ((13, "value"), (15, "errors"),) # Fields of the results returned by $arresult? where type = 2 _ARRESULT_TYPE2_ENTRY_MAP = ( - (9, 'reading-type'), # 0 = glucose blood strip, - # 1 = ketone blood strip, - # 2 = glucose sensor - (12, 'value'), - (15, 'sport-flag'), - (16, 'medication-flag'), - (17, 'rapid-acting-flag'), # see _ARRESULT_RAPID_INSULIN_ENTRY_MAP - (18, 'long-acting-flag'), - (19, 'custom-comments-bitfield'), - (23, 'double-long-acting-insulin'), - (25, 'food-flag'), - (26, 'food-carbs-grams'), - (28, 'errors'), + (9, "reading-type"), # 0 = glucose blood strip, + # 1 = ketone blood strip, + # 2 = glucose sensor + (12, "value"), + (15, "sport-flag"), + (16, "medication-flag"), + (17, "rapid-acting-flag"), # see _ARRESULT_RAPID_INSULIN_ENTRY_MAP + (18, "long-acting-flag"), + (19, "custom-comments-bitfield"), + (23, "double-long-acting-insulin"), + (25, "food-flag"), + (26, "food-carbs-grams"), + (28, "errors"), ) _ARRESULT_TIME_ADJUSTMENT_ENTRY_MAP = ( - (9, 'old_month'), - (10, 'old_day'), - (11, 'old_year'), - (12, 'old_hour'), - (13, 'old_minute'), - (14, 'old_second'), + (9, "old_month"), + (10, "old_day"), + (11, "old_year"), + (12, "old_hour"), + (13, "old_minute"), + (14, "old_second"), ) # Fields only valid when rapid-acting-flag is "1" -_ARRESULT_RAPID_INSULIN_ENTRY_MAP = ( - (43, 'double-rapid-acting-insulin'), -) +_ARRESULT_RAPID_INSULIN_ENTRY_MAP = ((43, "double-rapid-acting-insulin"),) def _parse_record(record, entry_map): @@ -82,26 +77,25 @@ def _parse_record(record, entry_map): return {} try: - return { - key: int(record[idx]) for idx, key in entry_map - } + return {key: int(record[idx]) for idx, key in entry_map} except IndexError: return {} -def _extract_timestamp(parsed_record, prefix=''): +def _extract_timestamp(parsed_record, prefix=""): """Extract the timestamp from a parsed record. This leverages the fact that all the records have the same base structure. """ return datetime.datetime( - parsed_record[prefix + 'year'] + 2000, - parsed_record[prefix + 'month'], - parsed_record[prefix + 'day'], - parsed_record[prefix + 'hour'], - parsed_record[prefix + 'minute'], - parsed_record[prefix + 'second']) + parsed_record[prefix + "year"] + 2000, + parsed_record[prefix + "month"], + parsed_record[prefix + "day"], + parsed_record[prefix + "hour"], + parsed_record[prefix + "minute"], + parsed_record[prefix + "second"], + ) def _parse_arresult(record): @@ -112,24 +106,23 @@ def _parse_arresult(record): # There are other record types, but we don't currently need to expose these. if not parsed_record: return None - elif parsed_record['type'] == 2: + elif parsed_record["type"] == 2: parsed_record.update(_parse_record(record, _ARRESULT_TYPE2_ENTRY_MAP)) - elif parsed_record['type'] == 5: + elif parsed_record["type"] == 5: parsed_record.update(_parse_record(record, _ARRESULT_TIME_ADJUSTMENT_ENTRY_MAP)) return common.TimeAdjustment( _extract_timestamp(parsed_record), - _extract_timestamp(parsed_record, 'old_'), - extra_data={'device_id': parsed_record['device_id']}, + _extract_timestamp(parsed_record, "old_"), + extra_data={"device_id": parsed_record["device_id"]}, ) else: return None # Check right away if we have rapid insulin - if parsed_record['rapid-acting-flag']: - parsed_record.update( - _parse_record(record, _ARRESULT_RAPID_INSULIN_ENTRY_MAP)) + if parsed_record["rapid-acting-flag"]: + parsed_record.update(_parse_record(record, _ARRESULT_RAPID_INSULIN_ENTRY_MAP)) - if parsed_record['errors']: + if parsed_record["errors"]: return None comment_parts = [] @@ -137,68 +130,69 @@ def _parse_arresult(record): cls = None value = None - if parsed_record['reading-type'] == 2: - comment_parts.append('(Scan)') + if parsed_record["reading-type"] == 2: + comment_parts.append("(Scan)") measure_method = common.MeasurementMethod.CGM cls = common.GlucoseReading - value = parsed_record['value'] - elif parsed_record['reading-type'] == 0: - comment_parts.append('(Blood)') + value = parsed_record["value"] + elif parsed_record["reading-type"] == 0: + comment_parts.append("(Blood)") measure_method = common.MeasurementMethod.BLOOD_SAMPLE cls = common.GlucoseReading - value = parsed_record['value'] - elif parsed_record['reading-type'] == 1: - comment_parts.append('(Ketone)') + value = parsed_record["value"] + elif parsed_record["reading-type"] == 1: + comment_parts.append("(Ketone)") measure_method = common.MeasurementMethod.BLOOD_SAMPLE cls = common.KetoneReading # automatically convert the raw value in mmol/L - value = freestyle.convert_ketone_unit(parsed_record['value']) + value = freestyle.convert_ketone_unit(parsed_record["value"]) else: # unknown reading return None custom_comments = record[29:35] for comment_index in range(6): - if parsed_record['custom-comments-bitfield'] & (1 << comment_index): + if parsed_record["custom-comments-bitfield"] & (1 << comment_index): comment_parts.append(custom_comments[comment_index][1:-1]) - if parsed_record['sport-flag']: - comment_parts.append('Sport') + if parsed_record["sport-flag"]: + comment_parts.append("Sport") - if parsed_record['medication-flag']: - comment_parts.append('Medication') + if parsed_record["medication-flag"]: + comment_parts.append("Medication") - if parsed_record['food-flag']: - if parsed_record['food-carbs-grams']: - comment_parts.append( - 'Food (%d g)' % parsed_record['food-carbs-grams']) + if parsed_record["food-flag"]: + if parsed_record["food-carbs-grams"]: + comment_parts.append("Food (%d g)" % parsed_record["food-carbs-grams"]) else: - comment_parts.append('Food') + comment_parts.append("Food") - if parsed_record['long-acting-flag']: - if parsed_record['double-long-acting-insulin']: + if parsed_record["long-acting-flag"]: + if parsed_record["double-long-acting-insulin"]: comment_parts.append( - 'Long-acting insulin (%.1f)' % - (parsed_record['double-long-acting-insulin']/2.)) + "Long-acting insulin (%.1f)" + % (parsed_record["double-long-acting-insulin"] / 2.0) + ) else: - comment_parts.append('Long-acting insulin') + comment_parts.append("Long-acting insulin") - if parsed_record['rapid-acting-flag']: + if parsed_record["rapid-acting-flag"]: # provide default value, as this record does not always exist # (even if rapid-acting-flag is set) - if parsed_record.get('double-rapid-acting-insulin', 0): + if parsed_record.get("double-rapid-acting-insulin", 0): comment_parts.append( - 'Rapid-acting insulin (%.1f)' % - (parsed_record['double-rapid-acting-insulin']/2.)) + "Rapid-acting insulin (%.1f)" + % (parsed_record["double-rapid-acting-insulin"] / 2.0) + ) else: - comment_parts.append('Rapid-acting insulin') + comment_parts.append("Rapid-acting insulin") return cls( _extract_timestamp(parsed_record), value, - comment='; '.join(comment_parts), + comment="; ".join(comment_parts), measure_method=measure_method, - extra_data={'device_id': parsed_record['device_id']}, + extra_data={"device_id": parsed_record["device_id"]}, ) @@ -210,16 +204,16 @@ class Device(freestyle.FreeStyleHidDevice): def get_meter_info(self): """Return the device information in structured form.""" return common.MeterInfo( - 'FreeStyle Libre', + "FreeStyle Libre", serial_number=self.get_serial_number(), - version_info=( - 'Software version: ' + self._get_version(),), + version_info=("Software version: " + self._get_version(),), native_unit=self.get_glucose_unit(), - patient_name=self.get_patient_name()) + patient_name=self.get_patient_name(), + ) def get_serial_number(self): """Overridden function as the command is not compatible.""" - return self._send_text_command(b'$sn?').rstrip('\r\n') + return self._send_text_command(b"$sn?").rstrip("\r\n") def get_glucose_unit(self): # pylint: disable=no-self-use """Returns the glucose unit of the device.""" @@ -231,27 +225,27 @@ class Device(freestyle.FreeStyleHidDevice): # First of all get the usually longer list of sensor readings, and # convert them to Readings objects. - for record in self._get_multirecord(b'$history?'): + for record in self._get_multirecord(b"$history?"): parsed_record = _parse_record(record, _HISTORY_ENTRY_MAP) - if not parsed_record or parsed_record['errors'] != 0: + if not parsed_record or parsed_record["errors"] != 0: # The reading is considered invalid, so ignore it. continue yield common.GlucoseReading( _extract_timestamp(parsed_record), - parsed_record['value'], - comment='(Sensor)', + parsed_record["value"], + comment="(Sensor)", measure_method=common.MeasurementMethod.CGM, - extra_data={'device_id': parsed_record['device_id']}, + extra_data={"device_id": parsed_record["device_id"]}, ) # Then get the results of explicit scans and blood tests (and other # events). - for record in self._get_multirecord(b'$arresult?'): + for record in self._get_multirecord(b"$arresult?"): reading = _parse_arresult(record) if reading: yield reading def zero_log(self): - self._send_text_command(b'$resetpatient') + self._send_text_command(b"$resetpatient") diff --git a/glucometerutils/drivers/fsoptium.py b/glucometerutils/drivers/fsoptium.py index 66b23ca..5c3971e 100644 --- a/glucometerutils/drivers/fsoptium.py +++ b/glucometerutils/drivers/fsoptium.py @@ -20,13 +20,13 @@ import datetime import logging import re -from glucometerutils import common -from glucometerutils import exceptions -from glucometerutils.support import serial, driver_base +from glucometerutils import common, exceptions +from glucometerutils.support import driver_base, serial _CLOCK_RE = re.compile( - r'^Clock:\t(?P[A-Z][a-z]{2}) (?P[0-9]{2}) (?P[0-9]{4})\t' - r'(?P