├── .dir-locals.el ├── .github └── workflows │ ├── pre-commit.yml │ └── pytest.yml ├── .gitignore ├── .mailmap ├── .mergify.yml ├── .pre-commit-config.yaml ├── .pylintrc ├── AUTHORS ├── LICENSES ├── 0BSD.txt └── MIT.txt ├── README.md ├── glucometer.py ├── glucometerutils ├── __init__.py ├── common.py ├── driver.py ├── drivers │ ├── __init__.py │ ├── accuchek_reports.py │ ├── contourusb.py │ ├── fsfreedomlite.py │ ├── fsinsulinx.py │ ├── fslibre.py │ ├── fslibre2.py │ ├── fsoptium.py │ ├── fsprecisionneo.py │ ├── glucomenareo.py │ ├── otultra2.py │ ├── otultraeasy.py │ ├── otverio2015.py │ ├── otverioiq.py │ ├── py.typed │ ├── sdcodefree.py │ ├── td42xx.py │ └── tests │ │ ├── __init__.py │ │ ├── test_contourusb.py │ │ ├── test_fsoptium.py │ │ ├── test_otultra2.py │ │ ├── test_otultraeasy.py │ │ └── test_td42xx.py ├── exceptions.py ├── glucometer.py ├── py.typed ├── support │ ├── __init__.py │ ├── construct_extras.py │ ├── contourusb.py │ ├── freestyle.py │ ├── freestyle_libre.py │ ├── hiddevice.py │ ├── lifescan.py │ ├── lifescan_binary_protocol.py │ ├── py.typed │ ├── serial.py │ └── tests │ │ ├── __init__.py │ │ ├── test_construct_extras.py │ │ └── test_lifescan.py └── tests │ ├── __init__.py │ └── test_common.py ├── mypy.ini ├── pyproject.toml ├── setup.cfg ├── setup.py └── udev └── 69-glucometerutils.rules /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ; SPDX-FileCopyrightText: © 2017 The glucometerutils Authors 2 | ; SPDX-License-Identifier: MIT 3 | ((nil . ((fill-column . 88) 4 | (whitespace-line-column . 88))) 5 | (markdown-mode . ((mode . flyspell))) 6 | (python-mode . ((mode . flyspell-prog) 7 | (mode . whitespace)))) 8 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Copyright (c) 2019 Anthony Sottile 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: pre-commit 6 | 7 | on: 8 | pull_request: 9 | push: 10 | branches: [main] 11 | 12 | jobs: 13 | pre-commit: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-python@v3 18 | - uses: pre-commit/action@v3.0.0 19 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: 0BSD 4 | 5 | name: pytest 6 | 7 | on: 8 | push: 9 | pull_request: 10 | 11 | jobs: 12 | pytest: 13 | 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | python-version: [3.9, "3.10", 3.11] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | sudo apt install libusb-1.0-0-dev libudev-dev 29 | pip install .[all] 30 | - name: Test with pytest 31 | run: | 32 | pytest -vvv --mypy 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | *.egg-info/ 6 | *.pyc 7 | *~ 8 | .cache 9 | .mypy_cache/ 10 | .vscode/ 11 | /MANIFEST 12 | /dist/ 13 | __pycache__/ 14 | build 15 | .idea/ 16 | *venv/ 17 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | Diego Elio Pettenò 6 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | pull_request_rules: 6 | - name: Automatic merge on approval 7 | conditions: 8 | - or: 9 | - "#approved-reviews-by>=1" 10 | - "author=Flameeyes" 11 | - "status-success=pytest (ubuntu-latest, 3.9)" 12 | - "status-success=pytest (ubuntu-latest, 3.10)" 13 | - "status-success=pytest (ubuntu-latest, 3.11)" 14 | - "status-success=pre-commit" 15 | actions: 16 | merge: 17 | method: rebase 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.4.0 8 | hooks: 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/PyCQA/isort 13 | rev: 5.12.0 14 | hooks: 15 | - id: isort 16 | additional_dependencies: 17 | - toml 18 | - repo: https://github.com/psf/black 19 | rev: 23.7.0 20 | hooks: 21 | - id: black 22 | - repo: https://github.com/PyCQA/flake8 23 | rev: 6.1.0 24 | hooks: 25 | - id: flake8 26 | - repo: https://github.com/fsfe/reuse-tool 27 | rev: v2.1.0 28 | hooks: 29 | - id: reuse 30 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | [MESSAGE CONTROL] 6 | disable= 7 | duplicate-code, 8 | too-few-public-methods, 9 | too-many-branches, 10 | too-many-locals, 11 | too-many-nested-blocks, 12 | too-many-return-statements, 13 | too-many-statements, 14 | 15 | [BASIC] 16 | good-names = i, j, k, e, _ 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2013 The glucometerutils Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # This is the list of glucometerutils authors for copyright purposes. 6 | # 7 | # This does not necessarily list everyone who has contributed code, since in 8 | # some cases, their employer may be the copyright holder. To see the full list 9 | # of contributors, see the revision history in source control. 10 | Anders Hammarquist 11 | Andreas Sandberg 12 | André Caldas 13 | Arkadiusz Bulski 14 | Benjamin Schäfer 15 | Christos Arvanitis 16 | Diego Elio Pettenò 17 | Dorian Scholz 18 | Jim Sifferle 19 | L. Guruprasad 20 | Leonard Lausen 21 | Mathieu Grivois 22 | Muhammad Kaisar Arkhan 23 | Naokazu Terada 24 | Noel Cragg 25 | Red Daly 26 | Ryan Jarvis 27 | Samuel Martin 28 | Stefanie Tellex 29 | Warren Moore 30 | Wesley T. Honeycutt 31 | -------------------------------------------------------------------------------- /LICENSES/0BSD.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2006 by Rob Landley 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 11 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 |

8 | GitHub 9 | Code style: black 10 |

11 | 12 | # Glucometer Utilities 13 | 14 | This repository includes a command line utility to interact with a number of 15 | blood sugar meters (glucometer) models from various manufacturers. 16 | 17 | While support varies by device, the actions that may be available are as 18 | follows: 19 | 20 | * `info` shows the model, serial number, date and time, and configured glucose 21 | unit of the device. 22 | * `dump` export the recorded blood sugar or β-ketone readings from the device 23 | in comma-separated values format. 24 | * `datetime` reads or updates the date and time of the device clock. 25 | * `zero` deletes all the recorded readings (only implemented for few devices). 26 | 27 | ## Example Usage 28 | 29 | Most of the drivers require optional dependencies, and those are listed in the 30 | table below. If you do not want to install the dependencies manually, you should 31 | be able to set this up using `virtualenv` and `pip`: 32 | 33 | ```shell 34 | $ python3 -m venv $(pwd)/glucometerutils-venv 35 | $ . glucometerutils-venv/bin/activate 36 | (glucometerutils-venv) $ DRIVER=myglucometer-driver # see table below 37 | (glucometerutils-venv) $ pip install "git+https://github.com/glucometers-tech/glucometerutils.git#egg=glucometerutils[${DRIVER}]" 38 | (glucometerutils-venv) $ glucometer --driver ${DRIVER} help 39 | ``` 40 | 41 | ## Supported devices 42 | 43 | Please see the following table for the driver for each device that is known and 44 | supported. 45 | 46 | | Manufacturer | Model Name | Driver | Dependencies | 47 | | --- | --- | --- | --- | 48 | | LifeScan | OneTouch Ultra 2 | `otultra2` | [pyserial] | 49 | | LifeScan | OneTouch Ultra Easy | `otultraeasy` | [construct] [pyserial] | 50 | | LifeScan | OneTouch Ultra Mini | `otultraeasy` | [construct] [pyserial] | 51 | | LifeScan | OneTouch Verio IQ | `otverioiq` | [construct] [pyserial] | 52 | | LifeScan | OneTouch Verio (USB) | `otverio2015` | [construct] [python-scsi] | 53 | | LifeScan | OneTouch Select Plus | `otverio2015` | [construct] [python-scsi] | 54 | | LifeScan | OneTouch Select Plus Flex¹ | `otverio2015` | [construct] [python-scsi] | 55 | | Abbott | FreeStyle Freedom Lite† | `fsfreedomlite` | [pyserial] | 56 | | Abbott | FreeStyle InsuLinx† | `fsinsulinx` | [freestyle-hid] [hidapi]‡ | 57 | | Abbott | FreeStyle Libre | `fslibre` | [freestyle-hid] [hidapi]‡ | 58 | | Abbott | FreeStyle Libre 2 | `fslibre2` | [freestyle-hid] [freestyle-keys] [hidapi]‡ | 59 | | Abbott | FreeStyle Optium | `fsoptium` | [pyserial] | 60 | | Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ | 61 | | Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ | 62 | | Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ | 63 | | Roche | Accu-Chek Mobile | `accuchek_reports` | | 64 | | SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] | 65 | | TaiDoc | TD-4277 | `td42xx` | [construct] [pyserial]² [hidapi] | 66 | | TaiDoc | TD-4235B | `td42xx` | [construct] [pyserial]² [hidapi] | 67 | | GlucoRx | Nexus | `td42xx` | [construct] [pyserial]² [hidapi] | 68 | | GlucoRx | NexusQ | `td42xx` | [construct] [pyserial]² [hidapi] | 69 | | Menarini | GlucoMen Nexus | `td42xx` | [construct] [pyserial]² [hidapi] | 70 | | Aktivmed | GlucoCheck XL | `td42xx` | [construct] [pyserial]² [hidapi] | 71 | | Ascensia | ContourUSB | `contourusb` | [construct] [hidapi]‡ | 72 | | Menarini | GlucoMen areo³ | `glucomenareo` | [pyserial] [crcmod] | 73 | 74 | † Untested. 75 | 76 | ‡ Optional dependency on Linux; required on other operating systems. 77 | 78 | ¹ USB only, bluetooth not supported. 79 | 80 | ² Requires a version of pyserial supporting CP2110 bridges. Supported starting 81 | from version 3.5. 82 | 83 | ³ Serial cable only, NFC not supported. 84 | 85 | To identify the supported features for each of the driver, query the `help` 86 | action: 87 | 88 | glucometer.py --driver fslibre help 89 | 90 | If you have knowledge of a protocol of a glucometer you would have supported, 91 | please provide a reference, possibly by writing a specification and contribute 92 | it to https://protocols.glucometers.tech/ . 93 | 94 | [construct]: https://construct.readthedocs.io/en/latest/ 95 | [freestyle-hid]: https://pypi.org/project/freestyle-hid/ 96 | [freestyle-keys]: https://pypi.org/project/freestyle-keys/ 97 | [pyserial]: https://pythonhosted.org/pyserial/ 98 | [python-scsi]: https://pypi.org/project/PYSCSI/ 99 | [hidapi]: https://pypi.python.org/pypi/hidapi 100 | [crcmod]: https://pypi.org/project/crcmod/ 101 | 102 | ## Dump format 103 | 104 | The `dump` action by default will output CSV-compatible format, with the 105 | following fields: 106 | 107 | * date and time; 108 | * meter reading value; 109 | * before/after meal information, if known; 110 | * comment provided with the reading, if any. 111 | 112 | Meal and comment information is provided by the meters supporting the 113 | information. In the future, meal information could be guessed based on the time 114 | of the reading. 115 | 116 | The unit format used by the dump by default matches what the meter reports as 117 | its display unit, which might differ from the one used by the meter for internal 118 | representation and wire protocol. You can override the display unit with 119 | `--unit`. 120 | 121 | ## Development 122 | 123 | The tool is being written keeping in mind that different glucometers, 124 | even if they are all from the same manufacturer, will use different 125 | protocols. 126 | 127 | If you want to contribute code, please note that the target language 128 | is Python 3.9, and that the style to follow is for the most part PEP8 129 | compatible. 130 | 131 | To set up your development environment follow these guidelines: 132 | 133 | ```shell 134 | $ git clone https://github.com/glucometers-tech/glucometerutils.git 135 | $ cd glucometerutils 136 | $ python3 -m venv --python=python3.7 137 | $ . venv/bin/activate 138 | $ pip install -e .[dev] 139 | $ # If you want to work on a specific driver specify this after dev e.g. 140 | $ # pip install -e .[dev,myglucometer-driver] # see table above 141 | $ pre-commit install 142 | ``` 143 | 144 | ## License 145 | 146 | Copyright © 2013-2020 The glucometerutils Authors 147 | 148 | Permission is hereby granted, free of charge, to any person obtaining 149 | a copy of this software and associated documentation files (the 150 | "Software"), to deal in the Software without restriction, including 151 | without limitation the rights to use, copy, modify, merge, publish, 152 | distribute, sublicense, and/or sell copies of the Software, and to 153 | permit persons to whom the Software is furnished to do so, subject to 154 | the following conditions: 155 | 156 | The above copyright notice and this permission notice shall be 157 | included in all copies or substantial portions of the Software. 158 | 159 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 160 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 161 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 162 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 163 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 164 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 165 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 166 | -------------------------------------------------------------------------------- /glucometer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- python -*- 3 | # 4 | # SPDX-FileCopyrightText: © 2013 The glucometerutils Authors 5 | # SPDX-License-Identifier: MIT 6 | 7 | from glucometerutils import glucometer 8 | 9 | if __name__ == "__main__": 10 | glucometer.main() 11 | -------------------------------------------------------------------------------- /glucometerutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glucometers-tech/glucometerutils/9b87a5b854f5b46f9d7fbc4dbdb12dbfae072ed1/glucometerutils/__init__.py -------------------------------------------------------------------------------- /glucometerutils/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SPDX-FileCopyrightText: © 2013 The glucometerutils Authors 4 | # SPDX-License-Identifier: MIT 5 | """Common routines for data in glucometers.""" 6 | 7 | import datetime 8 | import enum 9 | import textwrap 10 | from collections.abc import Sequence 11 | from typing import Any, Optional, Union 12 | 13 | import attr 14 | 15 | 16 | class Unit(enum.Enum): 17 | MG_DL = "mg/dL" 18 | MMOL_L = "mmol/L" 19 | 20 | 21 | # Constants for meal information 22 | class Meal(enum.Enum): 23 | NONE = "" 24 | BEFORE = "Before Meal" 25 | AFTER = "After Meal" 26 | 27 | 28 | # Constants for measure method 29 | class MeasurementMethod(enum.Enum): 30 | BLOOD_SAMPLE = "blood sample" 31 | CGM = "CGM" # Continuous Glucose Monitoring 32 | TIME = "time" 33 | 34 | 35 | def convert_glucose_unit(value: float, from_unit: Unit, to_unit: Unit) -> float: 36 | """Convert the given value of glucose level between units. 37 | 38 | Args: 39 | value: The value of glucose in the current unit 40 | from_unit: The unit value is currently expressed in 41 | to_unit: The unit to conver the value to: the other if empty. 42 | 43 | Returns: 44 | The converted representation of the blood glucose level. 45 | """ 46 | from_unit = Unit(from_unit) 47 | to_unit = Unit(to_unit) 48 | 49 | if from_unit == to_unit: 50 | return value 51 | 52 | if from_unit == Unit.MG_DL: 53 | return round(value / 18.0, 2) 54 | 55 | return round(value * 18.0, 1) 56 | 57 | 58 | @attr.s(auto_attribs=True) 59 | class GlucoseReading: 60 | timestamp: datetime.datetime 61 | value: float 62 | meal: Meal = attr.ib(default=Meal.NONE, validator=attr.validators.in_(Meal)) 63 | comment: str = "" 64 | measure_method: MeasurementMethod = attr.ib( 65 | default=MeasurementMethod.BLOOD_SAMPLE, 66 | validator=attr.validators.in_(MeasurementMethod), 67 | ) 68 | extra_data: dict[str, Any] = attr.Factory(dict) 69 | 70 | def get_value_as(self, to_unit: Unit) -> float: 71 | """Returns the reading value as the given unit. 72 | 73 | Args: 74 | to_unit: The unit to return the value to. 75 | """ 76 | return convert_glucose_unit(self.value, Unit.MG_DL, to_unit) 77 | 78 | def as_csv(self, unit: Unit) -> str: 79 | """Returns the reading as a formatted comma-separated value string.""" 80 | return '"%s","%.2f","%s","%s","%s"' % ( 81 | self.timestamp, 82 | self.get_value_as(unit), 83 | self.meal.value, 84 | self.measure_method.value, 85 | self.comment, 86 | ) 87 | 88 | 89 | @attr.s(auto_attribs=True) 90 | class KetoneReading: 91 | timestamp: datetime.datetime 92 | value: float 93 | comment: str = "" 94 | measure_method: MeasurementMethod = attr.ib( 95 | default=MeasurementMethod.BLOOD_SAMPLE, 96 | validator=attr.validators.in_({MeasurementMethod.BLOOD_SAMPLE}), 97 | ) 98 | extra_data: dict[str, Any] = attr.Factory(dict) 99 | 100 | def as_csv(self, unit: Unit) -> str: 101 | """Returns the reading as a formatted comma-separated value string.""" 102 | del unit # Unused for Ketone readings. 103 | 104 | return '"%s","%.2f","","%s","%s"' % ( 105 | self.timestamp, 106 | self.value, 107 | self.measure_method.value, 108 | self.comment, 109 | ) 110 | 111 | 112 | @attr.s(auto_attribs=True) 113 | class TimeAdjustment: 114 | timestamp: datetime.datetime 115 | old_timestamp: datetime.datetime 116 | measure_method: MeasurementMethod = attr.ib( 117 | default=MeasurementMethod.TIME, validator=attr.validators.in_(MeasurementMethod) 118 | ) 119 | extra_data: dict[str, Any] = attr.Factory(dict) 120 | 121 | def as_csv(self, unit: Unit) -> str: 122 | del unit 123 | return '"%s","","","%s","%s"' % ( 124 | self.timestamp, 125 | self.measure_method.value, 126 | self.old_timestamp, 127 | ) 128 | 129 | 130 | AnyReading = Union[GlucoseReading, KetoneReading, TimeAdjustment] 131 | 132 | 133 | @attr.s(auto_attribs=True) 134 | class MeterInfo: 135 | """General information about the meter. 136 | 137 | Attributes: 138 | model: Human readable model name, chosen by the driver. 139 | serial_number: Serial number identified for the reader (or N/A if not 140 | available in the protocol.) 141 | version_info: List of strings with any version information available about 142 | the device. It can include hardware and software version. 143 | native_unit: One of the Unit values to identify the meter native unit. 144 | """ 145 | 146 | model: str 147 | serial_number: str = "N/A" 148 | version_info: Sequence[str] = () 149 | native_unit: Unit = attr.ib(default=Unit.MG_DL, validator=attr.validators.in_(Unit)) 150 | patient_name: Optional[str] = None 151 | 152 | def __str__(self) -> str: 153 | version_information_string = "N/A" 154 | if self.version_info: 155 | version_information_string = "\n ".join( 156 | self.version_info 157 | ).strip() 158 | 159 | base_output = textwrap.dedent( 160 | f"""\ 161 | {self.model} 162 | Serial Number: {self.serial_number} 163 | Version Information: 164 | {version_information_string} 165 | Native Unit: {self.native_unit.value} 166 | """ 167 | ) 168 | 169 | if self.patient_name is not None: 170 | base_output += f"Patient Name: {self.patient_name}\n" 171 | 172 | return base_output 173 | -------------------------------------------------------------------------------- /glucometerutils/driver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SPDX-FileCopyrightText: © 2020 The glucometerutils Authors 4 | # SPDX-License-Identifier: MIT 5 | 6 | import abc 7 | import dataclasses 8 | import datetime 9 | import importlib 10 | import inspect 11 | from collections.abc import Generator 12 | from typing import Optional 13 | 14 | from glucometerutils import common 15 | 16 | 17 | class GlucometerDevice(abc.ABC): 18 | def __init__(self, device_path: Optional[str]) -> None: 19 | pass 20 | 21 | def connect(self) -> None: 22 | pass 23 | 24 | def disconnect(self) -> None: 25 | pass 26 | 27 | @abc.abstractmethod 28 | def get_meter_info(self) -> common.MeterInfo: 29 | """Return the device information in structured form.""" 30 | pass 31 | 32 | @abc.abstractmethod 33 | def get_serial_number(self) -> str: 34 | pass 35 | 36 | @abc.abstractmethod 37 | def get_glucose_unit(self) -> common.Unit: 38 | """Returns the glucose unit of the device.""" 39 | pass 40 | 41 | @abc.abstractmethod 42 | def get_datetime(self) -> datetime.datetime: 43 | pass 44 | 45 | def set_datetime( 46 | self, date: Optional[datetime.datetime] = None 47 | ) -> datetime.datetime: 48 | """Sets the date and time of the glucometer. 49 | 50 | Args: 51 | date: The value to set the date/time of the glucometer to. If none is 52 | given, the current date and time of the computer is used. 53 | 54 | Returns: 55 | A datetime object built according to the returned response. 56 | """ 57 | if not date: 58 | date = datetime.datetime.now() 59 | return self._set_device_datetime(date) 60 | 61 | @abc.abstractmethod 62 | def _set_device_datetime(self, date: datetime.datetime) -> datetime.datetime: 63 | pass 64 | 65 | @abc.abstractmethod 66 | def zero_log(self) -> None: 67 | pass 68 | 69 | @abc.abstractmethod 70 | def get_readings(self) -> Generator[common.AnyReading, None, None]: 71 | pass 72 | 73 | 74 | @dataclasses.dataclass 75 | class Driver: 76 | device: type[GlucometerDevice] 77 | help: str 78 | 79 | 80 | def load_driver(driver_name: str) -> Driver: 81 | driver_module = importlib.import_module(f"glucometerutils.drivers.{driver_name}") 82 | help_string = inspect.getdoc(driver_module) 83 | assert help_string is not None 84 | 85 | return Driver(getattr(driver_module, "Device"), help_string) 86 | -------------------------------------------------------------------------------- /glucometerutils/drivers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glucometers-tech/glucometerutils/9b87a5b854f5b46f9d7fbc4dbdb12dbfae072ed1/glucometerutils/drivers/__init__.py -------------------------------------------------------------------------------- /glucometerutils/drivers/accuchek_reports.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SPDX-FileCopyrightText: © 2016 The glucometerutils Authors 4 | # SPDX-License-Identifier: MIT 5 | """Driver for Accu-Chek Mobile devices with reports mode. 6 | 7 | Supported features: 8 | - get readings, including comments; 9 | - use the glucose unit preset on the device by default; 10 | - get serial number. 11 | 12 | Expected device path: /mnt/ACCUCHEK, the mountpoint of the block device. 13 | 14 | The Accu-Chek Mobile meters should be set to "Reports" mode. 15 | 16 | """ 17 | 18 | import csv 19 | import datetime 20 | import glob 21 | import os 22 | from collections.abc import Generator 23 | from typing import NoReturn, Optional 24 | 25 | from glucometerutils import common, driver, exceptions 26 | 27 | _UNIT_MAP = { 28 | "mmol/l": common.Unit.MMOL_L, 29 | "mg/dl": common.Unit.MG_DL, 30 | } 31 | 32 | _DATE_CSV_KEY = "Date" 33 | _TIME_CSV_KEY = "Time" 34 | _RESULT_CSV_KEY = "Result" 35 | _UNIT_CSV_KEY = "Unit" 36 | _TEMPWARNING_CSV_KEY = "Temperature warning" # ignored 37 | _OUTRANGE_CSV_KEY = "Out of target range" # ignored 38 | _OTHER_CSV_KEY = "Other" # ignored 39 | _BEFORE_MEAL_CSV_KEY = "Before meal" 40 | _AFTER_MEAL_CSV_KEY = "After meal" 41 | # Control test has extra whitespace which is not ignored. 42 | _CONTROL_CSV_KEY = "Control test" + " " * 197 43 | 44 | _DATE_FORMAT = "%d.%m.%Y" 45 | _TIME_FORMAT = "%H:%M" 46 | 47 | _DATETIME_FORMAT = " ".join((_DATE_FORMAT, _TIME_FORMAT)) 48 | 49 | 50 | class Device(driver.GlucometerDevice): 51 | def __init__(self, device: Optional[str]) -> None: 52 | if not device or not os.path.isdir(device): 53 | raise exceptions.CommandLineError( 54 | "--device parameter is required, should point to mount path " 55 | "for the meter." 56 | ) 57 | 58 | reports_path = os.path.join(device, "*", "Reports", "*.csv") 59 | report_files = glob.glob(reports_path) 60 | if not report_files: 61 | raise exceptions.ConnectionFailed( 62 | f'No report file found in path "{reports_path}".' 63 | ) 64 | 65 | self.report_file = report_files[0] 66 | 67 | def _get_records_reader(self) -> csv.DictReader: 68 | self.report.seek(0) 69 | # Skip the first two lines 70 | next(self.report) 71 | next(self.report) 72 | 73 | return csv.DictReader( 74 | self.report, delimiter=";", skipinitialspace=True, quoting=csv.QUOTE_NONE 75 | ) 76 | 77 | def connect(self) -> None: 78 | self.report = open(self.report_file, "r", newline="\r\n", encoding="utf-8") 79 | 80 | def disconnect(self) -> None: 81 | self.report.close() 82 | 83 | def get_meter_info(self) -> common.MeterInfo: 84 | return common.MeterInfo( 85 | f"{self.get_model()} glucometer", 86 | serial_number=self.get_serial_number(), 87 | native_unit=self.get_glucose_unit(), 88 | ) 89 | 90 | def get_model(self) -> str: 91 | # $device/MODEL/Reports/*.csv 92 | return os.path.basename(os.path.dirname(os.path.dirname(self.report_file))) 93 | 94 | def get_serial_number(self) -> str: 95 | self.report.seek(0) 96 | # ignore the first line. 97 | next(self.report) 98 | # The second line of the CSV is serial-no;report-date;report-time;;;;;;; 99 | return next(self.report).split(";")[0] 100 | 101 | def get_glucose_unit(self) -> common.Unit: 102 | # Get the first record available and parse that. 103 | record = next(self._get_records_reader()) 104 | return _UNIT_MAP[record[_UNIT_CSV_KEY]] 105 | 106 | def get_datetime(self) -> NoReturn: 107 | raise NotImplementedError 108 | 109 | def _set_device_datetime(self, date: datetime.datetime) -> NoReturn: 110 | raise NotImplementedError 111 | 112 | def zero_log(self) -> NoReturn: 113 | raise NotImplementedError 114 | 115 | def _extract_datetime( 116 | self, record: dict[str, str] 117 | ) -> datetime.datetime: # pylint: disable=no-self-use 118 | # Date and time are in separate column, but we want to parse them 119 | # together. 120 | date_and_time = " ".join((record[_DATE_CSV_KEY], record[_TIME_CSV_KEY])) 121 | return datetime.datetime.strptime(date_and_time, _DATETIME_FORMAT) 122 | 123 | def _extract_meal( 124 | self, record: dict[str, str] 125 | ) -> common.Meal: # pylint: disable=no-self-use 126 | if record[_AFTER_MEAL_CSV_KEY] and record[_BEFORE_MEAL_CSV_KEY]: 127 | raise exceptions.InvalidResponse("Reading cannot be before and after meal.") 128 | elif record[_AFTER_MEAL_CSV_KEY]: 129 | return common.Meal.AFTER 130 | elif record[_BEFORE_MEAL_CSV_KEY]: 131 | return common.Meal.BEFORE 132 | else: 133 | return common.Meal.NONE 134 | 135 | def get_readings(self) -> Generator[common.AnyReading, None, None]: 136 | for record in self._get_records_reader(): 137 | if record[_RESULT_CSV_KEY] is None: 138 | continue 139 | 140 | yield common.GlucoseReading( 141 | self._extract_datetime(record), 142 | common.convert_glucose_unit( 143 | float(record[_RESULT_CSV_KEY]), 144 | _UNIT_MAP[record[_UNIT_CSV_KEY]], 145 | common.Unit.MG_DL, 146 | ), 147 | meal=self._extract_meal(record), 148 | ) 149 | -------------------------------------------------------------------------------- /glucometerutils/drivers/contourusb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SPDX-FileCopyrightText: © 2019 The glucometerutils Authors 4 | # SPDX-License-Identifier: MIT 5 | """Driver for ContourUSB devices. 6 | 7 | Supported features: 8 | - get readings (blood glucose), including comments; 9 | - get date and time; 10 | - get serial number and software version; 11 | - get device info (e.g. unit) 12 | 13 | Expected device path: /dev/hidraw4 or similar HID device. Optional when using 14 | HIDAPI. 15 | 16 | Further information on the device protocol can be found at 17 | 18 | http://protocols.ascensia.com/Programming-Guide.aspx 19 | 20 | """ 21 | 22 | import datetime 23 | from collections.abc import Generator 24 | from typing import NoReturn, Optional 25 | 26 | from glucometerutils import common 27 | from glucometerutils.support import contourusb 28 | 29 | 30 | def _extract_timestamp(parsed_record: dict[str, str]): 31 | """Extract the timestamp from a parsed record. 32 | 33 | This leverages the fact that all the reading records have the same base structure. 34 | """ 35 | datetime_str = parsed_record["datetime"] 36 | 37 | return datetime.datetime( 38 | int(datetime_str[0:4]), # year 39 | int(datetime_str[4:6]), # month 40 | int(datetime_str[6:8]), # day 41 | int(datetime_str[8:10]), # hour 42 | int(datetime_str[10:12]), # minute 43 | 0, 44 | ) 45 | 46 | 47 | class Device(contourusb.ContourHidDevice): 48 | """Glucometer driver for Contour devices.""" 49 | 50 | def __init__(self, device: Optional[str]) -> None: 51 | super().__init__((0x1A79, 0x6002), device) 52 | 53 | def get_meter_info(self) -> common.MeterInfo: 54 | self._get_info_record() 55 | return common.MeterInfo( 56 | "Contour USB", 57 | serial_number=self._get_serial_number(), 58 | version_info=("Meter versions: " + self._get_version(),), 59 | native_unit=self.get_glucose_unit(), 60 | ) 61 | 62 | def get_glucose_unit(self) -> common.Unit: 63 | if self._get_glucose_unit() == "0": 64 | return common.Unit.MG_DL 65 | else: 66 | return common.Unit.MMOL_L 67 | 68 | def get_readings(self) -> Generator[common.AnyReading, None, None]: 69 | """ 70 | Get reading dump from download data mode(all readings stored) 71 | This meter supports only blood samples 72 | """ 73 | for parsed_record in self._get_multirecord(): 74 | yield common.GlucoseReading( 75 | _extract_timestamp(parsed_record), 76 | int(parsed_record["value"]), 77 | comment=parsed_record["markers"], 78 | measure_method=common.MeasurementMethod.BLOOD_SAMPLE, 79 | ) 80 | 81 | def get_serial_number(self) -> NoReturn: 82 | raise NotImplementedError 83 | 84 | def _set_device_datetime(self, date: datetime.datetime) -> NoReturn: 85 | raise NotImplementedError 86 | 87 | def zero_log(self) -> NoReturn: 88 | raise NotImplementedError 89 | -------------------------------------------------------------------------------- /glucometerutils/drivers/fsfreedomlite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SPDX-FileCopyrightText: © 2021 Stefanie Tellex 4 | # SPDX-License-Identifier: MIT 5 | """Driver for FreeStyle Freedom Lite devices. 6 | 7 | Supported features: 8 | - get readings 9 | - assumes the device uses mg/dL for the glucose unit 10 | - get date and time; 11 | - get serial number and software version. 12 | 13 | Expected device path: /dev/ttyUSB0 or similar serial port device. 14 | 15 | Further information on the device protocol can be found at 16 | 17 | https://protocols.glucometers.tech/abbott/freestyle-lite.html 18 | """ 19 | 20 | import datetime 21 | import logging 22 | import re 23 | from typing import Generator, NoReturn, Sequence 24 | 25 | from glucometerutils import common, driver, exceptions 26 | from glucometerutils.support import serial 27 | 28 | _CLOCK_INIT_RE = re.compile( 29 | r"^(?P[A-Z][a-z]{2}) (?P[0-9]{2}) (?P[0-9]{4}) " 30 | r"(?P