# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: MIT
"""Driver for Accu-Chek Mobile devices with reports mode.
Supported features:
- get readings, including comments;
- use the glucose unit preset on the device by default;
- get serial number.
Expected device path: /mnt/ACCUCHEK, the mountpoint of the block device.
The Accu-Chek Mobile meters should be set to "Reports" mode.
"""
import csv
import datetime
import glob
import os
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,
}
_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
_DATE_FORMAT = "%d.%m.%Y"
_TIME_FORMAT = "%H:%M"
_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."
)
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
)
self.report_file = report_files[0]
def _get_records_reader(self):
self.report.seek(0)
# Skip the first two lines
next(self.report)
next(self.report)
return csv.DictReader(
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")
def disconnect(self):
self.report.close()
def get_meter_info(self):
return common.MeterInfo(
"%s glucometer" % self.get_model(),
serial_number=self.get_serial_number(),
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)))
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]
def get_glucose_unit(self):
# Get the first record available and parse that.
record = next(self._get_records_reader())
return _UNIT_MAP[record[_UNIT_CSV_KEY]]
def get_datetime(self):
raise NotImplementedError
def _set_device_datetime(self, date):
raise NotImplementedError
def zero_log(self):
raise NotImplementedError
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]))
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.")
elif record[_AFTER_MEAL_CSV_KEY]:
return common.Meal.AFTER
elif record[_BEFORE_MEAL_CSV_KEY]:
return common.Meal.BEFORE
else:
return common.Meal.NONE
def get_readings(self):
for record in self._get_records_reader():
if record[_RESULT_CSV_KEY] is None:
continue
yield common.GlucoseReading(
self._extract_datetime(record),
common.convert_glucose_unit(
float(record[_RESULT_CSV_KEY]),
_UNIT_MAP[record[_UNIT_CSV_KEY]],
common.Unit.MG_DL,
),
meal=self._extract_meal(record),
)