#!/usr/bin/env python3 import argparse import configparser import logging import os import signal import inspect import sys import time from datetime import datetime from libreview import LibreViewSession from screens import DebugScreen from screens import TerminalScreen import requests # CONSTANTS DEFAULT_INTERVAL = 5 DEFAULT_FONT_SIZE = 9 DEFAULT_DEBUG_SCREEN_WIDTH = 128 DEFAULT_DEBUG_SCREEN_HEIGHT = 32 DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS = '1' SUPPORTED_SCREENS = [ "debug", "terminal", "waveshare_2.23_ssd1305" ] # GLOBAL VARIABLES SHOULD_EXIT = False def file_exists(file_path): return os.path.isfile(file_path) def directory_exists(directory_path): return os.path.isdir(directory_path) def signal_handler(signum, frame): global SHOULD_EXIT if SHOULD_EXIT: logging.info("already received exit signal exiting NOW") sys.exit(1) SHOULD_EXIT = True logging.info(f"received {signal.Signals(signum).name} signal shutting down") logging.debug("stack trace:") for frame, filename, lineno, function, _, _ in inspect.getouterframes(frame): logging.debug(f"file: {filename}, line_number: {lineno}, function: {function}") def main(): global SHOULD_EXIT logging.basicConfig( stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) parser = argparse.ArgumentParser( prog='glucose-monitor', description='A program to display glucose levels and if you blood sugar is high' ) parser.add_argument('-t', '--token') parser.add_argument('-c', '--config') args = parser.parse_args() if args.config is not None: if not file_exists(args.config): logging.fatal("config file location {args.config} does not exist") sys.exit(1) else: config_file_location = args.config else: possible_config_file_locations = [ str(os.getenv("HOME")) + "/.config/glucose-monitor/config.ini", "/etc/glucose-monitor/config.ini", "config.ini" ] config_file_location = "" for possible_config_file_location in possible_config_file_locations: if file_exists(possible_config_file_location): config_file_location = possible_config_file_location logging.info("config file found at " + config_file_location) break if config_file_location == "": logging.fatal("could not find a config file") sys.exit(1) config = configparser.ConfigParser() config.read(config_file_location) if 'LOG' in config: if 'LEVEL' in config['LOG']: level = config['LOG']['LEVEL'].lower() if level == "debug": logging.getLogger().setLevel(logging.DEBUG) elif level == "info": logging.getLogger().setLevel(logging.INFO) elif level == "warning": logging.getLogger().setLevel(logging.WARNING) elif level == "error": logging.getLogger().setLevel(logging.ERROR) elif level == "critical": logging.getLogger().setLevel(logging.CRITICAL) else: logging.error("log level specified in config file is not valid") logging.debug("set log level to debug") if args.token is None: if 'CREDENTIALS' not in config: # TODO: support credentials in environmental variables logging.fatal("no credentials specified in config file") sys.exit(1) if 'EMAIL' not in config['CREDENTIALS']: # TODO: support credentials in environmental variables logging.fatal("no email specified in config file") sys.exit(1) if 'PASSWORD' not in config['CREDENTIALS']: # TODO: support credentials in environmental variables logging.fatal("no password specified in config file") sys.exit(1) delay = DEFAULT_INTERVAL font = "DEFAULT" font_size = DEFAULT_FONT_SIZE if 'GENERAL' in config: general_config = config['GENERAL'] if 'INTERVAL' in general_config: delay = int(config['GENERAL']['INTERVAL']) if 'UPPER_BOUND' in general_config: upper_bound = config['GENERAL']['UPPER_BOUND'] if 'LOWER_BOUND' in general_config: lower_bound = config['GENERAL']['LOWER_BOUND'] if 'FONT' in general_config: font = general_config['FONT'] logging.debug("found font in config, checking that it exists") if not file_exists(font): logging.fatal(f"font does not exist at path {font}") sys.exit(1) else: logging.debug(f"found font at {font}") if 'FONT_SIZE' in general_config: font_size = general_config['FONT_SIZE'] display = TerminalScreen() if 'SCREEN' in config: screen_config = config['SCREEN'] if 'TYPE' not in screen_config: logging.fatal("no screen type in config file") sys.exit(1) screen_type = screen_config['TYPE'].lower() if screen_type not in SUPPORTED_SCREENS: logging.fatal("sorry screen type not supported") sys.exit(1) if screen_type == "waveshare_2.23_ssd1305": from screens.waveshare_233_ssd1305 import Waveshare_233_SSD1305 display = Waveshare_233_SSD1305() elif screen_type == "debug": width = DEFAULT_DEBUG_SCREEN_WIDTH height = DEFAULT_DEBUG_SCREEN_HEIGHT number_of_colour_bits = DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS if 'WIDTH' in screen_config: width = int(screen_config['WIDTH']) if 'HEIGHT' in screen_config: height = int(screen_config['HEIGHT']) if 'NUMBER_OF_COLOUR_BITS' in screen_config: number_of_colour_bits = screen_config['NUMBER_OF_COLOUR_BITS'] display = DebugScreen(width, height, number_of_colour_bits) libreview_session = LibreViewSession() if args.token is not None: libreview_session.useToken(args.token) else: libreview_session.authenticate(config['CREDENTIALS']['EMAIL'], config['CREDENTIALS']['PASSWORD']) if libreview_session.authenticated and not SHOULD_EXIT: logging.info("successfully authenticated with LibreView") else: logging.fatal("failed to authenticate with LibreView") sys.exit(1) try: libreview_session.getConnections() except Exception as e: logging.debug("got exception,", e) logging.fatal("failed to get connections") sys.exit(1) if len(libreview_session.connections) > 1: logging.fatal("currently glucose-monitor only supports accounts with one connection") sys.exit(1) if len(libreview_session.connections) < 1: logging.fatal("to use glucose-monitor your libreview account must have at least one connection") sys.exit(1) patient_id = libreview_session.connections[0]['patientId'] if display.supports_custom_fonts: if font != "DEFAULT": display.setFont(font, int(font_size)) else: display.setDefaultFont() while not SHOULD_EXIT: display.clearScreen() try: graph = libreview_session.getGraph(patient_id) graph['connection']['glucoseMeasurement'] except requests.exceptions.ConnectionError: logging.error("failed to get details about patient, connection error") continue except requests.exceptions.HTTPError: logging.error("failed to get details about patient, API returned invalid response") continue except requests.exceptions.RequestException: logging.error("failed to get details about patient an unknown error occurred") continue except KeyError: logging.error("the data returned from the API was not in an expected format") continue glucoseMeasurement = graph['connection']['glucoseMeasurement'] now = datetime.now() lines = [ f"{now.strftime("%H:%M:%S")}", str(glucoseMeasurement['Value']) ] display.displayLines(lines) time.sleep(delay) logging.debug("exiting now...") if __name__ == "__main__": main()