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

@ -14,10 +14,11 @@ from datetime import datetime
from libreview import LibreViewSession from libreview import LibreViewSession
from screens import DebugScreen from screens import DebugScreen
from screens import TerminalScreen from screens import Screen
import requests import requests
import yaml
# CONSTANTS # CONSTANTS
@ -25,7 +26,7 @@ DEFAULT_INTERVAL = 5
DEFAULT_FONT_SIZE = 9 DEFAULT_FONT_SIZE = 9
DEFAULT_DEBUG_SCREEN_WIDTH = 128 DEFAULT_DEBUG_SCREEN_WIDTH = 128
DEFAULT_DEBUG_SCREEN_HEIGHT = 32 DEFAULT_DEBUG_SCREEN_HEIGHT = 32
DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS = '1' DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS = "1"
SUPPORTED_SCREENS = [ SUPPORTED_SCREENS = [
"debug", "debug",
"terminal", "terminal",
@ -37,14 +38,119 @@ SUPPORTED_SCREENS = [
SHOULD_EXIT = False 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) return os.path.isfile(file_path)
def directory_exists(directory_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) 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): def signal_handler(signum, frame):
global SHOULD_EXIT global SHOULD_EXIT
if SHOULD_EXIT: if SHOULD_EXIT:
@ -63,19 +169,20 @@ def main():
logging.basicConfig( logging.basicConfig(
stream=sys.stdout, stream=sys.stdout,
level=logging.INFO, 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.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog='glucose-monitor', prog="glucose-monitor",
description='A program to display glucose levels and if you blood sugar is high' description="A program to display glucose levels and if you blood sugar is high"
) )
parser.add_argument('-t', '--token') parser.add_argument("-m", "--migrate", action="store_true")
parser.add_argument('-c', '--config') parser.add_argument("-t", "--token")
parser.add_argument("-c", "--config")
args = parser.parse_args() args = parser.parse_args()
@ -87,28 +194,43 @@ def main():
config_file_location = args.config config_file_location = args.config
else: else:
possible_config_file_locations = [ possible_config_file_locations = [
str(os.getenv("HOME")) + "/.config/glucose-monitor/config.ini", str(os.getenv("HOME")) + "/.config/glucose-monitor/config.yaml",
"/etc/glucose-monitor/config.ini", "/etc/glucose-monitor/config.yaml",
"config.ini" "config.yaml"
] ]
config_file_location = "" config_file_location = ""
found_config_file = False
for possible_config_file_location in possible_config_file_locations: for possible_config_file_location in possible_config_file_locations:
if file_exists(possible_config_file_location): if file_exists(possible_config_file_location):
config_file_location = possible_config_file_location config_file_location = possible_config_file_location
found_config_file = True
logging.info("config file found at " + config_file_location) logging.info("config file found at " + config_file_location)
break break
if config_file_location == "": if not found_config_file:
logging.fatal("could not find a config file") old_config_file_locations = [
sys.exit(1) 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() with open(config_file_location, "r") as config_file:
config.read(config_file_location) config = yaml.safe_load(config_file)
if 'LOG' in config: if "log" in config:
if 'LEVEL' in config['LOG']: if "level" in config["log"]:
level = config['LOG']['LEVEL'].lower() level = config["log"]["level"].lower()
if level == "debug": if level == "debug":
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
elif level == "info": elif level == "info":
@ -125,15 +247,15 @@ def main():
logging.debug("set log level to debug") logging.debug("set log level to debug")
if args.token is None: if args.token is None:
if 'CREDENTIALS' not in config: if "credentials" not in config:
# TODO: support credentials in environmental variables # TODO: support credentials in environmental variables
logging.fatal("no credentials specified in config file") logging.fatal("no credentials specified in config file")
sys.exit(1) sys.exit(1)
if 'EMAIL' not in config['CREDENTIALS']: if "email" not in config["credentials"]:
# TODO: support credentials in environmental variables # TODO: support credentials in environmental variables
logging.fatal("no email specified in config file") logging.fatal("no email specified in config file")
sys.exit(1) sys.exit(1)
if 'PASSWORD' not in config['CREDENTIALS']: if "password" not in config["credentials"]:
# TODO: support credentials in environmental variables # TODO: support credentials in environmental variables
logging.fatal("no password specified in config file") logging.fatal("no password specified in config file")
sys.exit(1) sys.exit(1)
@ -141,35 +263,54 @@ def main():
delay = DEFAULT_INTERVAL delay = DEFAULT_INTERVAL
font = "DEFAULT" font = "DEFAULT"
font_size = DEFAULT_FONT_SIZE font_size = DEFAULT_FONT_SIZE
if 'GENERAL' in config: lines = [
general_config = config['GENERAL'] "%T",
if 'INTERVAL' in general_config: "GLUCOSE UNITS: %U"
delay = int(config['GENERAL']['INTERVAL']) ]
if "general" in config:
if 'UPPER_BOUND' in general_config: general_config = config["general"]
upper_bound = config['GENERAL']['UPPER_BOUND'] if "interval" in general_config:
if 'LOWER_BOUND' in general_config: delay = int(config["general"]["interval"])
lower_bound = config['GENERAL']['LOWER_BOUND']
# TODO: Add format specifier to check if high and low
if 'FONT' in general_config: # if "upper_bound" in general_config:
font = general_config['FONT'] # 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") logging.debug("found font in config, checking that it exists")
if not file_exists(font): if not file_exists(font):
logging.fatal(f"font does not exist at path {font}") logging.fatal(f"font does not exist at path {font}")
sys.exit(1) sys.exit(1)
else: else:
logging.debug(f"found font at {font}") logging.debug(f"found font at {font}")
if 'FONT_SIZE' in general_config: if "font_size" in general_config:
font_size = general_config['FONT_SIZE'] font_size = general_config["font_size"]
display = TerminalScreen() if "lines" in general_config:
if 'SCREEN' in config: lines = general_config["lines"]
screen_config = config['SCREEN']
if 'TYPE' not in screen_config: 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") logging.fatal("no screen type in config file")
sys.exit(1) sys.exit(1)
screen_type = screen_config['TYPE'].lower() screen_type = screen_config["type"].lower()
if screen_type not in SUPPORTED_SCREENS: if screen_type not in SUPPORTED_SCREENS:
logging.fatal("sorry screen type not supported") logging.fatal("sorry screen type not supported")
sys.exit(1) sys.exit(1)
@ -181,12 +322,12 @@ def main():
width = DEFAULT_DEBUG_SCREEN_WIDTH width = DEFAULT_DEBUG_SCREEN_WIDTH
height = DEFAULT_DEBUG_SCREEN_HEIGHT height = DEFAULT_DEBUG_SCREEN_HEIGHT
number_of_colour_bits = DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS number_of_colour_bits = DEFAULT_DEBUG_SCREEN_NUMBER_OF_COLOUR_BITS
if 'WIDTH' in screen_config: if "width" in screen_config:
width = int(screen_config['WIDTH']) width = int(screen_config["width"])
if 'HEIGHT' in screen_config: if "height" in screen_config:
height = int(screen_config['HEIGHT']) height = int(screen_config["height"])
if 'NUMBER_OF_COLOUR_BITS' in screen_config: if "number_of_colour_bits" in screen_config:
number_of_colour_bits = screen_config['NUMBER_OF_COLOUR_BITS'] number_of_colour_bits = screen_config["number_of_colour_bits"]
display = DebugScreen(width, height, number_of_colour_bits) display = DebugScreen(width, height, number_of_colour_bits)
@ -194,7 +335,7 @@ def main():
if args.token is not None: if args.token is not None:
libreview_session.useToken(args.token) libreview_session.useToken(args.token)
else: 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: if libreview_session.authenticated and not SHOULD_EXIT:
logging.info("successfully authenticated with LibreView") 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") logging.fatal("to use glucose-monitor your libreview account must have at least one connection")
sys.exit(1) sys.exit(1)
patient_id = libreview_session.connections[0]['patientId'] patient_id = libreview_session.connections[0]["patientId"]
if display.supports_custom_fonts: if display.supports_custom_fonts:
if font != "DEFAULT": if font != "DEFAULT":
@ -229,28 +370,26 @@ def main():
try: try:
graph = libreview_session.getGraph(patient_id) 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: except requests.exceptions.ConnectionError:
logging.error("failed to get details about patient, connection error") logging.error("failed to get details about patient, connection error")
continue
except requests.exceptions.HTTPError: except requests.exceptions.HTTPError:
logging.error("failed to get details about patient, API returned invalid response") logging.error("failed to get details about patient, API returned invalid response")
continue
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
logging.error("failed to get details about patient an unknown error occurred") logging.error("failed to get details about patient an unknown error occurred")
continue
except KeyError: except KeyError:
logging.error("the data returned from the API was not in an expected format") logging.error("the data returned from the API was not in an expected format")
continue
glucoseMeasurement = graph['connection']['glucoseMeasurement'] computed_output_lines = []
for output_line in output_lines:
now = datetime.now() computed_output_lines.append(eval(f"f'{output_line}'"))
lines = [ display.displayLines(computed_output_lines)
f"{now.strftime("%H:%M:%S")}",
str(glucoseMeasurement['Value'])
]
display.displayLines(lines)
time.sleep(delay) time.sleep(delay)

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

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

@ -1,3 +1,7 @@
from typing import List
import logging
class Screen: class Screen:
def __init__(self): def __init__(self):
self.type = None self.type = None
@ -8,7 +12,14 @@ class Screen:
return self.type return self.type
def clearScreen(self): 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") raise NotImplementedError("Subclass must implement this method")

@ -1,4 +1,5 @@
from typing import List from typing import List
import logging
import os import os
@ -7,11 +8,19 @@ from .screen import Screen
class TerminalScreen(Screen): class TerminalScreen(Screen):
def __init__(self): def __init__(self):
super().__init__()
self.type = "Terminal Screen" self.type = "Terminal Screen"
self.width, self.height = os.get_terminal_size() 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 # displays text on the screen
self.clearScreen() self.clearScreen()

@ -1,4 +1,5 @@
from typing import List from typing import List
import logging
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
@ -24,12 +25,12 @@ class Waveshare_233_SSD1305(Screen):
self.draw = ImageDraw.Draw(self.image) self.draw = ImageDraw.Draw(self.image)
def setDefaultFont(self): def setDefaultFont(self):
ImageFont.load_default() logging.debug("setting font to default font")
self.font = ImageFont.load_default()
def setFont(self, font, size): def setFont(self, font, size):
self.font = font
self.font_size = size self.font_size = size
ImageFont.truetype(font, size) self.font = ImageFont.truetype(font, size)
def clearBuffer(self): def clearBuffer(self):
self.draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0) 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.getbuffer(self.image)
self.ssd1305.ShowImage() 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 # displays text on the screen, the line that is first in the list is displayed on the top
y = 0 self.clearBuffer()
x = 0 self.clearScreen()
for i, line in lines:
self.draw.text((x, y+(i*self.font_size)), line, font=self.font, fill=255) 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() self.displayOnScreen()
def clearScreen(self): def clearScreen(self):

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