# -*- 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 from glucometerutils import exceptions _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: 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_datetime(self, date=None): 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))