summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDiego Elio Pettenò <flameeyes@flameeyes.com>2023-08-03 11:03:22 +0200
committermergify[bot] <37929162+mergify[bot]@users.noreply.github.com>2023-08-03 11:20:21 +0200
commit6410ef870bbe318b988fc2c20186fc96afb6e350 (patch)
tree0a3ec2983363d357e6086cb295da08e655c5dc6e
parentReformat with the new black. (diff)
downloadglucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.tar
glucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.tar.gz
glucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.tar.bz2
glucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.tar.lz
glucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.tar.xz
glucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.tar.zst
glucometerutils-6410ef870bbe318b988fc2c20186fc96afb6e350.zip
-rw-r--r--README.md52
-rw-r--r--glucometerutils/drivers/fslibre.py239
-rw-r--r--glucometerutils/drivers/fslibre2.py32
-rw-r--r--glucometerutils/support/freestyle.py2
-rw-r--r--glucometerutils/support/freestyle_libre.py252
-rw-r--r--setup.py1
6 files changed, 318 insertions, 260 deletions
diff --git a/README.md b/README.md
index 27a6b47..7635c94 100644
--- a/README.md
+++ b/README.md
@@ -43,31 +43,32 @@ $ . glucometerutils-venv/bin/activate
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` | [freestyle-hid] [hidapi]‡ |
-| Abbott | FreeStyle Libre | `fslibre` | [freestyle-hid] [hidapi]‡ |
-| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] |
-| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ |
-| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ |
-| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ |
-| Roche | Accu-Chek Mobile | `accuchek_reports` | |
-| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] |
-| TaiDoc | TD-4277 | `td42xx` | [construct] [pyserial]² [hidapi] |
-| TaiDoc | TD-4235B | `td42xx` | [construct] [pyserial]² [hidapi] |
-| GlucoRx | Nexus | `td42xx` | [construct] [pyserial]² [hidapi] |
-| GlucoRx | NexusQ | `td42xx` | [construct] [pyserial]² [hidapi] |
-| Menarini | GlucoMen Nexus | `td42xx` | [construct] [pyserial]² [hidapi] |
-| Aktivmed | GlucoCheck XL | `td42xx` | [construct] [pyserial]² [hidapi] |
-| Ascensia | ContourUSB | `contourusb` | [construct] [hidapi]‡ |
-| Menarini | GlucoMen areo³ | `glucomenareo` | [pyserial] [crcmod] |
+| 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` | [freestyle-hid] [hidapi]‡ |
+| Abbott | FreeStyle Libre | `fslibre` | [freestyle-hid] [hidapi]‡ |
+| Abbott | FreeStyle Libre 2 | `fslibre2` | [freestyle-hid] [freestyle-keys] [hidapi]‡ |
+| Abbott | FreeStyle Optium | `fsoptium` | [pyserial] |
+| Abbott | FreeStyle Precision Neo | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ |
+| Abbott | FreeStyle Optium Neo | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ |
+| Abbott | FreeStyle Optium Neo H | `fsprecisionneo` | [freestyle-hid] [hidapi]‡ |
+| Roche | Accu-Chek Mobile | `accuchek_reports` | |
+| SD Biosensor | SD CodeFree | `sdcodefree` | [construct] [pyserial] |
+| TaiDoc | TD-4277 | `td42xx` | [construct] [pyserial]² [hidapi] |
+| TaiDoc | TD-4235B | `td42xx` | [construct] [pyserial]² [hidapi] |
+| GlucoRx | Nexus | `td42xx` | [construct] [pyserial]² [hidapi] |
+| GlucoRx | NexusQ | `td42xx` | [construct] [pyserial]² [hidapi] |
+| Menarini | GlucoMen Nexus | `td42xx` | [construct] [pyserial]² [hidapi] |
+| Aktivmed | GlucoCheck XL | `td42xx` | [construct] [pyserial]² [hidapi] |
+| Ascensia | ContourUSB | `contourusb` | [construct] [hidapi]‡ |
+| Menarini | GlucoMen areo³ | `glucomenareo` | [pyserial] [crcmod] |
† Untested.
@@ -91,6 +92,7 @@ it to https://protocols.glucometers.tech/ .
[construct]: https://construct.readthedocs.io/en/latest/
[freestyle-hid]: https://pypi.org/project/freestyle-hid/
+[freestyle-keys]: https://pypi.org/project/freestyle-keys/
[pyserial]: https://pythonhosted.org/pyserial/
[python-scsi]: https://pypi.org/project/PYSCSI/
[hidapi]: https://pypi.python.org/pypi/hidapi
diff --git a/glucometerutils/drivers/fslibre.py b/glucometerutils/drivers/fslibre.py
index c11bab0..823e49c 100644
--- a/glucometerutils/drivers/fslibre.py
+++ b/glucometerutils/drivers/fslibre.py
@@ -20,244 +20,13 @@ https://protocols.glucometers.tech/abbott/freestyle-libre
"""
-import datetime
-import logging
-from typing import Dict, Generator, Mapping, Optional, Sequence, Tuple, Type
+from typing import Optional
-from glucometerutils import common
-from glucometerutils.support import freestyle
+from glucometerutils.support import freestyle_libre
-# 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"),
-)
-# Fields of the records returned by $history?
-_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"),
-)
-
-_ARRESULT_TIME_ADJUSTMENT_ENTRY_MAP = (
- (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"),)
-
-
-def _parse_record(
- record: Sequence[str], entry_map: Sequence[Tuple[int, str]]
-) -> Dict[str, int]:
- """Parses a list of string fields into a dictionary of integers."""
-
- if not record:
- return {}
-
- try:
- return {key: int(record[idx]) for idx, key in entry_map}
- except IndexError:
- return {}
-
-
-def _extract_timestamp(
- parsed_record: Mapping[str, int], prefix: str = ""
-) -> datetime.datetime:
- """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"],
- )
-
-
-def _parse_arresult(record: Sequence[str]) -> Optional[common.AnyReading]:
- """Takes an array of string fields as input and parses it into a Reading."""
-
- parsed_record = _parse_record(record, _BASE_ENTRY_MAP)
-
- # 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:
- parsed_record.update(_parse_record(record, _ARRESULT_TYPE2_ENTRY_MAP))
- 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"]},
- )
- 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["errors"]:
- return None
-
- comment_parts = []
- measure_method: Optional[common.MeasurementMethod] = None
- cls: Optional[Type[common.AnyReading]] = None
- value: Optional[float] = None
-
- 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)")
- measure_method = common.MeasurementMethod.BLOOD_SAMPLE
- cls = common.GlucoseReading
- 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
- raw_value = parsed_record["value"]
- if raw_value is None:
- raise ValueError(f"Invalid Ketone value: {parsed_record!r}")
- value = freestyle.convert_ketone_unit(raw_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):
- comment_parts.append(custom_comments[comment_index])
-
- if parsed_record["sport-flag"]:
- comment_parts.append("Sport")
-
- if parsed_record["medication-flag"]:
- comment_parts.append("Medication")
-
- if parsed_record["food-flag"]:
- grams = parsed_record["food-carbs-grams"]
- if grams:
- comment_parts.append(f"Food ({grams} g)")
- else:
- comment_parts.append("Food")
-
- if parsed_record["long-acting-flag"]:
- insulin = parsed_record["double-long-acting-insulin"] / 2
- if insulin:
- comment_parts.append(f"Long-acting insulin ({insulin:.1f})")
- else:
- comment_parts.append("Long-acting insulin")
-
- if parsed_record["rapid-acting-flag"]:
- # This record does not always exist, so calculate it only when present.
- if "double-rapid-acting-insulin" in parsed_record:
- rapid_insulin = parsed_record["double-rapid-acting-insulin"] / 2
- comment_parts.append(f"Rapid-acting insulin ({rapid_insulin:.1f})")
- else:
- comment_parts.append("Rapid-acting insulin")
-
- reading = cls(
- _extract_timestamp(parsed_record),
- value,
- comment="; ".join(comment_parts),
- measure_method=measure_method,
- extra_data={"device_id": parsed_record["device_id"]},
- )
-
- return reading
-
-
-class Device(freestyle.FreeStyleHidDevice):
- """Glucometer driver for FreeStyle Libre devices."""
+class Device(freestyle_libre.LibreDevice):
+ _MODEL_NAME = "FreeStyle Libre"
def __init__(self, device_path: Optional[str]) -> None:
super().__init__(0x3650, device_path, encoding="utf-8")
-
- def get_meter_info(self) -> common.MeterInfo:
- """Return the device information in structured form."""
- return common.MeterInfo(
- "FreeStyle Libre",
- serial_number=self.get_serial_number(),
- version_info=("Software version: " + self._get_version(),),
- native_unit=self.get_glucose_unit(),
- patient_name=self.get_patient_name(),
- )
-
- def get_serial_number(self) -> str:
- """Overridden function as the command is not compatible."""
- return self._session.send_text_command(b"$sn?").rstrip("\r\n")
-
- def get_glucose_unit(self) -> common.Unit: # pylint: disable=no-self-use
- """Returns the glucose unit of the device."""
- # TODO(Flameeyes): figure out how to identify the actual unit on the
- # device.
- return common.Unit.MG_DL
-
- def get_readings(self) -> Generator[common.AnyReading, None, None]:
- # First of all get the usually longer list of sensor readings, and
- # convert them to Readings objects.
- for record in self._session.query_multirecord(b"$history?"):
- parsed_record = _parse_record(record, _HISTORY_ENTRY_MAP)
-
- 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)",
- measure_method=common.MeasurementMethod.CGM,
- 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._session.query_multirecord(b"$arresult?"):
- logging.debug(f"Retrieved arresult: {record!r}")
- reading = _parse_arresult(record)
- if reading:
- yield reading
-
- def zero_log(self) -> None:
- self._session.send_text_command(b"$resetpatient")
diff --git a/glucometerutils/drivers/fslibre2.py b/glucometerutils/drivers/fslibre2.py
new file mode 100644
index 0000000..a8cd657
--- /dev/null
+++ b/glucometerutils/drivers/fslibre2.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# SPDX-FileCopyrightText: © 2023 The glucometerutils Authors
+# SPDX-License-Identifier: MIT
+"""Driver for FreeStyle Libre 2 devices.
+
+Supported features:
+ The same as the fslibre driver.
+
+Expected device path: /dev/hidraw9 or similar HID device. Optional when using
+HIDAPI.
+
+This driver is a shim on top of the fslibre driver, forcing encryption to be
+enabled for the session.
+
+Further information on the device protocol can be found at
+
+https://protocols.glucometers.tech/abbott/freestyle-libre
+https://protocols.glucometers.tech/abbott/freestyle-libre-2
+
+"""
+
+from typing import Optional
+
+from glucometerutils.support import freestyle_libre
+
+
+class Device(freestyle_libre.LibreDevice):
+ _MODEL_NAME = "FreeStyle Libre 2"
+
+ def __init__(self, device_path: Optional[str]) -> None:
+ super().__init__(0x3950, device_path, encoding="utf-8", encrypted=True)
diff --git a/glucometerutils/support/freestyle.py b/glucometerutils/support/freestyle.py
index b74d4b8..c93cead 100644
--- a/glucometerutils/support/freestyle.py
+++ b/glucometerutils/support/freestyle.py
@@ -51,6 +51,7 @@ class FreeStyleHidDevice(driver.GlucometerDevice):
text_cmd: int = 0x60,
text_reply_cmd: int = 0x60,
encoding: str = "ascii",
+ encrypted: bool = False,
) -> None:
super().__init__(device_path)
self._encoding = encoding
@@ -61,6 +62,7 @@ class FreeStyleHidDevice(driver.GlucometerDevice):
text_cmd,
text_reply_cmd,
encoding=encoding,
+ encrypted=encrypted,
)
except Exception as e:
raise exceptions.ConnectionFailed(str(e)) from e
diff --git a/glucometerutils/support/freestyle_libre.py b/glucometerutils/support/freestyle_libre.py
new file mode 100644
index 0000000..e5c9efe
--- /dev/null
+++ b/glucometerutils/support/freestyle_libre.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+#
+# SPDX-FileCopyrightText: © 2017 The glucometerutils Authors
+# SPDX-License-Identifier: MIT
+"""Base driver for FreeStyle Libre devices.
+
+This module provides the common driver functionality supported for Libre and Libre2.
+
+Protocol documentation available at
+https://protocols.glucometers.tech/abbott/freestyle-libre
+"""
+
+import datetime
+import logging
+from typing import Dict, Generator, Mapping, Optional, Sequence, Tuple, Type
+
+from glucometerutils import common
+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"),
+)
+
+# Fields of the records returned by $history?
+_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"),
+)
+
+_ARRESULT_TIME_ADJUSTMENT_ENTRY_MAP = (
+ (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"),)
+
+
+def _parse_record(
+ record: Sequence[str], entry_map: Sequence[Tuple[int, str]]
+) -> Dict[str, int]:
+ """Parses a list of string fields into a dictionary of integers."""
+
+ if not record:
+ return {}
+
+ try:
+ return {key: int(record[idx]) for idx, key in entry_map}
+ except IndexError:
+ return {}
+
+
+def _extract_timestamp(
+ parsed_record: Mapping[str, int], prefix: str = ""
+) -> datetime.datetime:
+ """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"],
+ )
+
+
+def _parse_arresult(record: Sequence[str]) -> Optional[common.AnyReading]:
+ """Takes an array of string fields as input and parses it into a Reading."""
+
+ parsed_record = _parse_record(record, _BASE_ENTRY_MAP)
+
+ # 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:
+ parsed_record.update(_parse_record(record, _ARRESULT_TYPE2_ENTRY_MAP))
+ 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"]},
+ )
+ 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["errors"]:
+ return None
+
+ comment_parts = []
+ measure_method: Optional[common.MeasurementMethod] = None
+ cls: Optional[Type[common.AnyReading]] = None
+ value: Optional[float] = None
+
+ 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)")
+ measure_method = common.MeasurementMethod.BLOOD_SAMPLE
+ cls = common.GlucoseReading
+ 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
+ raw_value = parsed_record["value"]
+ if raw_value is None:
+ raise ValueError(f"Invalid Ketone value: {parsed_record!r}")
+ value = freestyle.convert_ketone_unit(raw_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):
+ comment_parts.append(custom_comments[comment_index])
+
+ if parsed_record["sport-flag"]:
+ comment_parts.append("Sport")
+
+ if parsed_record["medication-flag"]:
+ comment_parts.append("Medication")
+
+ if parsed_record["food-flag"]:
+ grams = parsed_record["food-carbs-grams"]
+ if grams:
+ comment_parts.append(f"Food ({grams} g)")
+ else:
+ comment_parts.append("Food")
+
+ if parsed_record["long-acting-flag"]:
+ insulin = parsed_record["double-long-acting-insulin"] / 2
+ if insulin:
+ comment_parts.append(f"Long-acting insulin ({insulin:.1f})")
+ else:
+ comment_parts.append("Long-acting insulin")
+
+ if parsed_record["rapid-acting-flag"]:
+ # This record does not always exist, so calculate it only when present.
+ if "double-rapid-acting-insulin" in parsed_record:
+ rapid_insulin = parsed_record["double-rapid-acting-insulin"] / 2
+ comment_parts.append(f"Rapid-acting insulin ({rapid_insulin:.1f})")
+ else:
+ comment_parts.append("Rapid-acting insulin")
+
+ reading = cls(
+ _extract_timestamp(parsed_record),
+ value,
+ comment="; ".join(comment_parts),
+ measure_method=measure_method,
+ extra_data={"device_id": parsed_record["device_id"]},
+ )
+
+ return reading
+
+
+class LibreDevice(freestyle.FreeStyleHidDevice):
+ """Glucometer driver for FreeStyle Libre devices."""
+
+ _MODEL_NAME: str
+
+ def get_meter_info(self) -> common.MeterInfo:
+ """Return the device information in structured form."""
+ return common.MeterInfo(
+ self._MODEL_NAME,
+ serial_number=self.get_serial_number(),
+ version_info=("Software version: " + self._get_version(),),
+ native_unit=self.get_glucose_unit(),
+ patient_name=self.get_patient_name(),
+ )
+
+ def get_serial_number(self) -> str:
+ """Overridden function as the command is not compatible."""
+ return self._session.send_text_command(b"$sn?").rstrip("\r\n")
+
+ def get_glucose_unit(self) -> common.Unit: # pylint: disable=no-self-use
+ """Returns the glucose unit of the device."""
+ # TODO(Flameeyes): figure out how to identify the actual unit on the
+ # device.
+ return common.Unit.MG_DL
+
+ def get_readings(self) -> Generator[common.AnyReading, None, None]:
+ # First of all get the usually longer list of sensor readings, and
+ # convert them to Readings objects.
+ for record in self._session.query_multirecord(b"$history?"):
+ parsed_record = _parse_record(record, _HISTORY_ENTRY_MAP)
+
+ 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)",
+ measure_method=common.MeasurementMethod.CGM,
+ 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._session.query_multirecord(b"$arresult?"):
+ logging.debug(f"Retrieved arresult: {record!r}")
+ reading = _parse_arresult(record)
+ if reading:
+ yield reading
+
+ def zero_log(self) -> None:
+ self._session.send_text_command(b"$resetpatient")
diff --git a/setup.py b/setup.py
index e27b730..887fc52 100644
--- a/setup.py
+++ b/setup.py
@@ -14,6 +14,7 @@ extras_require = {
"contourusb": ["construct", "hidapi"],
"fsinsulinx": ["freestyle-hid>=1.0.2"],
"fslibre": ["freestyle-hid>=1.0.2"],
+ "fslibre2": ["freestyle-hid[encryption]>=1.1.0"],
"fsoptium": ["pyserial"],
"fsprecisionneo": ["freestyle-hid>=1.0.2"],
"glucomenareo": ["pyserial", "crcmod"],