Major progress, custom format now supported!

Plus various tidy up bits and bug fixes
devel
Ronald 1 year ago
parent 0b2c692392
commit 6ae804cd15

@ -1,4 +1,5 @@
import logging
from typing import Dict
import requests
@ -89,7 +90,7 @@ class LibreViewSession:
print(response.content)
self.connections = []
def getGraph(self, patientId):
def getGraph(self, patientId) -> Dict[str, Dict]:
"""
getGraph gets the LibreView graph for the {patientID}
"""
@ -108,7 +109,7 @@ class LibreViewSession:
if 'data' in json:
return json['data']
else:
return None
return {}

@ -14,10 +14,11 @@ from datetime import datetime
from libreview import LibreViewSession
from screens import DebugScreen
from screens import TerminalScreen
from screens import Screen
import requests
import yaml
# CONSTANTS
@ -25,7 +26,7 @@ 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'
DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS = "1"
SUPPORTED_SCREENS = [
"debug",
"terminal",
@ -37,14 +38,119 @@ SUPPORTED_SCREENS = [
SHOULD_EXIT = False
def file_exists(file_path):
def file_exists(file_path) -> bool:
"""
file_exists:
Checks if a file exists
Arguments:
file_path:
The file that we are looking for
Returns:
boolean:
Returns true if the file exists, and false if it doesn"t
"""
return os.path.isfile(file_path)
def directory_exists(directory_path):
"""
directory_exists:
Checks if a directory exists
Arguments:
directory_path:
The directory that we are looking for
Returns:
boolean:
Returns true if the file exists and is a directory, and false if it"s not
"""
return os.path.isdir(directory_path)
def ini_to_yaml(ini_file_path: str, yaml_file_path: str, lower_case: bool = False):
"""
ini_to_yaml:
Takes an ini file and produces a yaml file from it.
Arguments:
ini_file_path:
The path to the ini file that you want to convert to a yaml file
yaml_file_path:
The path to the yaml file that you wish to create
lower_case = False
Change all the sections and keys to lower case, if false leaves the case alone
Returns:
None
"""
config = configparser.ConfigParser()
config.read(ini_file_path)
d = {}
for section in config.sections():
if lower_case:
d[section.lower()] = {}
else:
d[section] = {}
for key, value in config[section].items():
if lower_case:
d[section.lower()][key.lower()] = value
else:
d[section][key] = value
with open(yaml_file_path, "w+") as file:
yaml.dump(d, file, default_flow_style=False)
def get_current_time(fmt: str = "%H:%M %d/%m/%Y") -> str:
"""
get_current_time:
Returns a string containing the date and/or time in the format specified in fmt
Arguments:
fmt:
The format of the date and/or time to be returned in the string
Returns:
Returns a string with the date and/or time in the format specified
"""
now = datetime.now()
return now.strftime(fmt)
def time_since(dt: datetime) -> str:
"""
time_since:
Returns a string with the difference between the datetime passed and today
Arguments:
dt:
A datetime class to get the time since
Returns:
A string in the with minutes if difference can be calculated in minutes otherwise will just return seconds
Will be in the format of "2m 30s" or "40s"
"""
now = datetime.now()
difference = now - dt
total_seconds = difference.total_seconds()
minutes = total_seconds // 60
seconds = int(total_seconds % 60)
if minutes > 0:
return f"{minutes}m {seconds}s"
return f"{seconds}s"
def signal_handler(signum, frame):
global SHOULD_EXIT
if SHOULD_EXIT:
@ -63,19 +169,20 @@ def main():
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
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'
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')
parser.add_argument("-m", "--migrate", action="store_true")
parser.add_argument("-t", "--token")
parser.add_argument("-c", "--config")
args = parser.parse_args()
@ -87,28 +194,43 @@ def main():
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"
str(os.getenv("HOME")) + "/.config/glucose-monitor/config.yaml",
"/etc/glucose-monitor/config.yaml",
"config.yaml"
]
config_file_location = ""
found_config_file = False
for possible_config_file_location in possible_config_file_locations:
if file_exists(possible_config_file_location):
config_file_location = possible_config_file_location
found_config_file = True
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)
if not found_config_file:
old_config_file_locations = [
str(os.getenv("HOME")) + "/.config/glucose-monitor/config.ini",
"/etc/glucose-monitor/config.ini",
"config.ini"
]
for old_config_file_location in old_config_file_locations:
if file_exists(old_config_file_location):
if not args.migrate:
logging.fatal("Sorry, ini files are no longer support, run glucose-monitor --migrate to move to yaml")
sys.exit(1)
else:
ini_to_yaml(old_config_file_location, old_config_file_location[:-4]+".yaml", lower_case=True)
config_file_location = old_config_file_location[:-4]+".yaml"
if config_file_location == "":
logging.fatal("could not find a config file")
sys.exit(1)
config = configparser.ConfigParser()
config.read(config_file_location)
with open(config_file_location, "r") as config_file:
config = yaml.safe_load(config_file)
if 'LOG' in config:
if 'LEVEL' in config['LOG']:
level = config['LOG']['LEVEL'].lower()
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":
@ -125,15 +247,15 @@ def main():
logging.debug("set log level to debug")
if args.token is None:
if 'CREDENTIALS' not in config:
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']:
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']:
if "password" not in config["credentials"]:
# TODO: support credentials in environmental variables
logging.fatal("no password specified in config file")
sys.exit(1)
@ -141,35 +263,54 @@ def main():
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']
lines = [
"%T",
"GLUCOSE UNITS: %U"
]
if "general" in config:
general_config = config["general"]
if "interval" in general_config:
delay = int(config["general"]["interval"])
# TODO: Add format specifier to check if high and low
# 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:
if "font_size" in general_config:
font_size = general_config["font_size"]
if "lines" in general_config:
lines = general_config["lines"]
output_lines = []
for line in lines:
temp = ""
if "%T" in line:
temp += line.replace("%T", '{get_current_time()}')
if "%U" in line:
temp += line.replace("%U", '{glucose_measurement["Value"]}')
if "%S" in line:
temp += line.replace("%S", '{time_since(glucose_measurement_timestamp_datetime)}')
output_lines.append(temp)
display = Screen()
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()
screen_type = screen_config["type"].lower()
if screen_type not in SUPPORTED_SCREENS:
logging.fatal("sorry screen type not supported")
sys.exit(1)
@ -181,12 +322,12 @@ def main():
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']
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)
@ -194,7 +335,7 @@ def main():
if args.token is not None:
libreview_session.useToken(args.token)
else:
libreview_session.authenticate(config['CREDENTIALS']['EMAIL'], config['CREDENTIALS']['PASSWORD'])
libreview_session.authenticate(config["credentials"]["email"], config["credentials"]["password"])
if libreview_session.authenticated and not SHOULD_EXIT:
logging.info("successfully authenticated with LibreView")
@ -216,7 +357,7 @@ def main():
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']
patient_id = libreview_session.connections[0]["patientId"]
if display.supports_custom_fonts:
if font != "DEFAULT":
@ -229,28 +370,26 @@ def main():
try:
graph = libreview_session.getGraph(patient_id)
graph['connection']['glucoseMeasurement']
graph["connection"]["glucoseMeasurement"]
glucose_measurement = graph["connection"]["glucoseMeasurement"]
glucose_measurement_timestamp = glucose_measurement["Timestamp"]
glucose_measurement_timestamp_datetime = datetime.strptime(
glucose_measurement_timestamp,
"%m/%d/%Y %I:%M:%S %p"
)
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)
computed_output_lines = []
for output_line in output_lines:
computed_output_lines.append(eval(f"f'{output_line}'"))
display.displayLines(computed_output_lines)
time.sleep(delay)

@ -1,3 +1,4 @@
from .screen import Screen
from .terminal_screen import TerminalScreen
from .debug_screens import DebugScreen
from .waveshare_233_ssd1305 import Waveshare_233_SSD1305

@ -1,6 +1,6 @@
import os
import logging
from typing import List
import logging
import os
import psutil
@ -23,13 +23,12 @@ class DebugScreen(Screen):
def setDefaultFont(self):
logging.debug("setting font to default font")
ImageFont.load_default()
self.font = ImageFont.load_default()
def setFont(self, font: str, size: int):
logging.debug(f"setting font to {font} with size, {size}")
self.font = font
self.font_size = size
ImageFont.truetype(font, size)
self.font = ImageFont.truetype(font, size)
def clearBuffer(self):
self.draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0)
@ -37,15 +36,35 @@ class DebugScreen(Screen):
def displayOnScreen(self):
self.image.show()
def displayLines(self, lines: List[str], x_padding: int = 0, y_padding: int = 4):
def displayLines(self, lines: List[str], x_margin: int = 2):
# displays text on the screen, the line that is first in the list is displayed on the top
self.clearBuffer()
self.clearScreen()
x_start = 0
x_start = x_margin
y_start = 0
x_padding = 0
y_padding = 0
for i, line in enumerate(lines):
self.draw.text((x_start+(i*x_padding), y_start+int(i*self.font_size)+(i*y_padding)), line, fill=255)
self.draw.text(
(
x_start+(i*x_padding),
y_start+y_padding
),
line,
font=self.font,
fill=255
)
bounding_box = self.draw.textbbox(
(
(x_start+x_padding),
y_start+y_padding,
),
line,
font=self.font,
)
bottom = bounding_box[3]
y_padding = bottom
self.displayOnScreen()
def clearScreen(self):

@ -1,3 +1,7 @@
from typing import List
import logging
class Screen:
def __init__(self):
self.type = None
@ -8,7 +12,14 @@ class Screen:
return self.type
def clearScreen(self):
self.__clearScreen()
raise NotImplementedError("Subclass must implement this method")
def setFont(self, font: str, size: int):
raise NotImplementedError("Subclass must implement this method")
def setDefaultFont(self):
raise NotImplementedError("Subclass must implement this method")
def __clearScreen(self):
def displayLines(self, lines: List[str], x_margin: int = 2):
logging.debug(f"Not implemented, not using {lines}, {x_margin}")
raise NotImplementedError("Subclass must implement this method")

@ -1,4 +1,5 @@
from typing import List
import logging
import os
@ -7,11 +8,19 @@ from .screen import Screen
class TerminalScreen(Screen):
def __init__(self):
super().__init__()
self.type = "Terminal Screen"
self.width, self.height = os.get_terminal_size()
def displayLines(self, lines: List[str], x_padding: int = 0, y_padding: int = 4):
def setDefaultFont(self):
return 0
def setFont(self, font: str, size: int):
logging.debug(f"TerminalScreen doesn't support custom fonts, can't set font to {font}, or size to {size}")
raise NotImplementedError("TerminalScreen doesn't support custom fonts")
def displayLines(self, lines: List[str], x_padding: int = 0, y_padding: int = 0):
# displays text on the screen
self.clearScreen()

@ -1,4 +1,5 @@
from typing import List
import logging
from PIL import Image, ImageDraw, ImageFont
@ -24,12 +25,12 @@ class Waveshare_233_SSD1305(Screen):
self.draw = ImageDraw.Draw(self.image)
def setDefaultFont(self):
ImageFont.load_default()
logging.debug("setting font to default font")
self.font = ImageFont.load_default()
def setFont(self, font, size):
self.font = font
self.font_size = size
ImageFont.truetype(font, size)
self.font = ImageFont.truetype(font, size)
def clearBuffer(self):
self.draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0)
@ -38,12 +39,35 @@ class Waveshare_233_SSD1305(Screen):
self.ssd1305.getbuffer(self.image)
self.ssd1305.ShowImage()
def displayLines(self, lines: List[str]):
def displayLines(self, lines: List[str], x_margin: int = 2):
# displays text on the screen, the line that is first in the list is displayed on the top
y = 0
x = 0
for i, line in lines:
self.draw.text((x, y+(i*self.font_size)), line, font=self.font, fill=255)
self.clearBuffer()
self.clearScreen()
x_start = x_margin
y_start = 0
x_padding = 0
y_padding = 0
for i, line in enumerate(lines):
self.draw.text(
(
x_start+(i*x_padding),
y_start+y_padding
),
line,
font=self.font,
fill=255
)
bounding_box = self.draw.textbbox(
(
(x_start+x_padding),
y_start+y_padding,
),
line,
font=self.font,
)
bottom = bounding_box[3]
y_padding = bottom
self.displayOnScreen()
def clearScreen(self):

@ -0,0 +1 @@
0.0.0.1
Loading…
Cancel
Save