diff --git a/.gitignore b/.gitignore index 894b10a..90d1a19 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /MANIFEST /dist/ __pycache__/ +.cache +build +*.egg-info/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1948c7f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 3.4 + - 3.5 + - 3.6 + +install: + - pip install -r test-requirements.txt + +script: + - py.test + - python setup.py bdist_wheel + - pip install ./dist/glucometerutils-*.whl diff --git a/README b/README index d87f3b8..0d90ea2 100644 --- a/README +++ b/README @@ -13,26 +13,41 @@ follows: * `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=project[${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` | [pyserial] | -| LifeScan | OneTouch Ultra Mini | `otultraeasy` | [pyserial] | -| LifeScan | OneTouch Verio (USB) | `otverio2015` | [python-scsi] | -| LifeScan | OneTouch Select Plus | `otverio2015` | [python-scsi] | -| Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [hidapi]‡ | -| Abbott | FreeStyle Libre | `fslibre` | [hidapi]‡ | -| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | -| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [hidapi]‡ | -| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [hidapi]‡ | -| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [hidapi]‡ | -| Roche | Accu-Chek Mobile | `accuchek_reports` | | -| SD Biosensor | SD CodeFree | `sdcodefree` | [pyserial] | +| 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] | +| 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] | † Untested. ‡ Optional dependency on Linux; required on other operating systems. @@ -46,6 +61,7 @@ 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://github.com/Flameeyes/glucometer-protocols/. +[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 diff --git a/glucometer.py b/glucometer.py index abed647..3b8bf70 100755 --- a/glucometer.py +++ b/glucometer.py @@ -1,150 +1,7 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""Utility to manage glucometers' data.""" +# -*- python -*- -__author__ = 'Diego Elio Pettenò' -__email__ = 'flameeyes@flameeyes.eu' -__copyright__ = 'Copyright © 2013-2017, Diego Elio Pettenò' -__license__ = 'MIT' - -import argparse -import importlib -import inspect -import logging -import sys - -from glucometerutils import common -from glucometerutils import exceptions - -def main(): - if sys.version_info < (3, 4): - raise Exception( - 'Unsupported Python version, please use at least Python 3.4') - - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest="action") - - parser.add_argument( - '--driver', action='store', required=True, - help='Select the driver to use for connecting to the glucometer.') - parser.add_argument( - '--device', action='store', required=False, - help=('Select the path to the glucometer device. Some devices require this ' - 'argument, others will try autodetection.')) - - parser.add_argument( - '--vlog', action='store', required=False, type=int, - help=('Python logging level. See the levels at ' - 'https://docs.python.org/3/library/logging.html#logging-levels')) - - subparsers.add_parser( - 'help', help=('Display a description of the driver, including supported ' - 'features and known quirks.')) - subparsers.add_parser( - 'info', help='Display information about the meter.') - subparsers.add_parser( - 'zero', help='Zero out the data log of the meter.') - - parser_dump = subparsers.add_parser( - 'dump', help='Dump the readings stored in the device.') - parser_dump.add_argument( - '--unit', action='store', - choices=[unit.value for unit in common.Unit], - help='Select the unit to use for the dumped data.') - parser_dump.add_argument( - '--sort-by', action='store', default='timestamp', - choices=common._ReadingBase._fields, - help='Field to order the dumped data by.') - parser_dump.add_argument( - '--with-ketone', action='store_true', default=False, - help='Enable ketone reading if available on the glucometer.') - - parser_date = subparsers.add_parser( - 'datetime', help='Reads or sets the date and time of the glucometer.') - parser_date.add_argument( - '--set', action='store', nargs='?', const='now', default=None, - help='Set the date rather than just reading it from the device.') - - args = parser.parse_args() - - logging.basicConfig(level=args.vlog) - - try: - driver = importlib.import_module('glucometerutils.drivers.' + args.driver) - except ImportError as e: - logging.error( - 'Error importing driver "%s", please check your --driver parameter:\n%s', - args.driver, e) - return 1 - - # This check needs to happen before we try to initialize the device, as the - # help action does not require a --device at all. - if args.action == 'help': - print(inspect.getdoc(driver)) - return 0 - - device = driver.Device(args.device) - - device.connect() - device_info = device.get_meter_info() - - try: - if args.action == 'info': - try: - time_str = device.get_datetime() - except NotImplementedError: - time_str = 'N/A' - print("{device_info}Time: {time}".format( - device_info=str(device_info), time=time_str)) - elif args.action == 'dump': - unit = args.unit - if unit is None: - unit = device_info.native_unit - - readings = device.get_readings() - - if not args.with_ketone: - readings = (reading for reading in readings - if not isinstance(reading, common.KetoneReading)) - - if args.sort_by is not None: - readings = sorted( - readings, key=lambda reading: getattr(reading, args.sort_by)) - - for reading in readings: - print(reading.as_csv(unit)) - elif args.action == 'datetime': - if args.set == 'now': - print(device.set_datetime()) - elif args.set: - try: - from dateutil import parser as date_parser - new_date = date_parser.parse(args.set) - except ImportError: - logging.error( - 'Unable to import module "dateutil", please install it.') - return 1 - except ValueError: - logging.error('%s: not a valid date', args.set) - return 1 - print(device.set_datetime(new_date)) - else: - print(device.get_datetime()) - elif args.action == 'zero': - confirm = input('Delete the device data log? (y/N) ') - if confirm.lower() in ['y', 'ye', 'yes']: - device.zero_log() - print('\nDevice data log zeroed.') - else: - print('\nDevice data log not zeroed.') - return 1 - else: - return 1 - except exceptions.Error as err: - print('Error while executing \'%s\': %s' % (args.action, str(err))) - return 1 - - device.disconnect() +from glucometerutils import glucometer if __name__ == "__main__": - main() + glucometer.main() diff --git a/glucometerutils/common.py b/glucometerutils/common.py index 318fc86..ba4f018 100644 --- a/glucometerutils/common.py +++ b/glucometerutils/common.py @@ -149,4 +149,4 @@ def __str__(self): Native Unit: {native_unit} """).format(model=self.model, serial_number=self.serial_number, version_information_string=version_information_string, - native_unit=self.native_unit) + native_unit=self.native_unit.value) diff --git a/glucometerutils/drivers/fslite.py b/glucometerutils/drivers/fslite.py new file mode 100644 index 0000000..fd4a461 --- /dev/null +++ b/glucometerutils/drivers/fslite.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- +"""Driver for FreeStyle Lite devices. + +Supported features: + - get readings (ignores ketone results); + - use the glucose unit preset on the device by default; + - get and set date and time; + - get serial number and software version. + +Expected device path: /dev/ttyUSB0 or similar serial port device. + +Further information on the device protocol can be found at + +http://www.flupzor.nl/protocol.html +""" + +__author__ = 'Diego Elio Pettenò, William Garber' +__email__ = 'flameeyes@flameeyes.eu, william.garber@att.net' +__copyright__ = 'Copyright © 2016-2017, Diego Elio Pettenò' +__license__ = 'MIT' + +import datetime +import logging +import re + +from glucometerutils import common +from glucometerutils import exceptions +from glucometerutils.support import serial + + +_CLOCK_RE = re.compile( + r'^(?P[A-Z][a-z]{2}) (?P[0-9]{2}) (?P[0-9]{4}) ' + r'(?P