diff --git a/bin/get_profile.py b/bin/get_profile.py old mode 100755 new mode 100644 index 2f3276d7d..68a1a26af --- a/bin/get_profile.py +++ b/bin/get_profile.py @@ -37,10 +37,20 @@ TIMED_ENTRIES = ['carbratio', 'sens', 'basal', 'target_low', 'target_high'] + def get_profiles(nightscout, token): + """Get profiles available in nightscout + + Gets profiles from nightscout, and returns them as a list of dictionaries. + + Args: + nightscout (str): Nightscout URL (required) + token (str): Nightscout token (optional) + + Returns: + list: A list of dictionaries, each containing a profile. """ - Get profiles available in nightscout - """ + r_url = nightscout + "/api/v1/profile.json" if token is not None: r_url = r_url + "?" + token @@ -48,10 +58,21 @@ def get_profiles(nightscout, token): return r.json() + def get_current_profile(nightscout, token, profile_name): + """Try to get the active profile + + Gets the active profile from nightscout, and then returns it. + + Args: + nightscout (str): Nightscout URL (required) + token (str): Nightscout token (optional) + profile_name (str): Profile name (optional) + + Returns: + dict: Profile """ - Try to get the active profile - """ + r_url = nightscout + "/api/v1/profile.json" if token is not None: r_url = r_url + "?" + token @@ -103,10 +124,21 @@ def get_current_profile(nightscout, token, profile_name): return p_list[0]["store"][profile_name] + def profiles(nightscout, token): + """Print list of profiles available in nightscout + + Gets profiles from nightscout, prints the default profile, and then + prints a list of all available profiles. + + Args: + nightscout (str): Nightscout URL (required) + token (str): Nightscout token (optional) + + Returns: + None """ - print list of profiles available in nightscout - """ + p_list = get_profiles(nightscout, token) default_profile = p_list[0]["defaultProfile"] profile_list = p_list[0]["store"].keys() @@ -116,10 +148,22 @@ def profiles(nightscout, token): print("\t" + profile) + def display(nightscout, token, profile_name, profile_format): + """Display contents of a profile, in requested format + + Gets the profile from nightscout, and then displays it in the requested format. + + Args: + nightscout (str): Nightscout URL (required) + token (str): Nightscout token (optional) + profile_name (str): Profile name (optional) + profile_format (str): Profile format (optional) + + Returns: + None """ - Display contents of a profile, in requested format - """ + profile = get_current_profile(nightscout, token, profile_name) if profile_format == "nightscout": # display_nightscout(p_list, profile_name) @@ -131,10 +175,22 @@ def display(nightscout, token, profile_name, profile_format): print(json.dumps(ns_to_oaps(profile), indent=4)) + def write(nightscout, token, profile_name, directory): + """Write profile in OpenAPS format to a directory + + Gets the profile from nightscout, and then writes it to the requested directory. + + Args: + nightscout (str): Nightscout URL (required) + token (str): Nightscout token (optional) + profile_name (str): Profile name (optional) + directory (str): Directory to write profile files to (required) + + Returns: + None """ - Write profile in OpenAPS format to a directory - """ + profile = ns_to_oaps(get_current_profile(nightscout, token, profile_name)) logging.debug("Checking for directory: %s", directory) if not os.path.isdir(directory): @@ -161,10 +217,17 @@ def write(nightscout, token, profile_name, directory): f.write(json.dumps(profile, indent=4)) + def normalize_entry(entry): + """Clean up an entry before further processing + + Args: + entry (dict): A single profile entry + + Returns: + None """ - Clean up an entry before further processing - """ + try: if entry["timeAsSeconds"]: pass @@ -186,10 +249,19 @@ def normalize_entry(entry): return entry + def ns_to_oaps(ns_profile): + """Convert nightscout profile to OpenAPS format + + Converts a nightscout profile to an OpenAPS profile. + + Args: + ns_profile (dict): Nightscout profile (required) + + Returns: + dict: OpenAPS profile """ - Convert nightscout profile to OpenAPS format - """ + oaps_profile = {} # XXX If addint any new entries, make sure to update PROFILE_KEYS at the top # Not represented in nightscout @@ -314,18 +386,33 @@ def ns_to_oaps(ns_profile): return sorted_profile + def display_nightscout(profile_data, profile_name): + """Display profile the way it comes from nightscout + + Args: + profile_data (dict): Profile data (required) + profile_name (str): Profile name (required) + + Returns: + None """ - Display profile the way it comes from nightscout - """ + print("Displaying profile {}".format(profile_name)) print(json.dumps(profile_data[0]["store"][profile_name], indent=4)) + def display_text(p_data): + """Display profile in text format + + Args: + p_data (dict): Profile data (required) + + Returns: + None """ - Display profile in text format - """ + # p_data = profile_data[0]["store"][profile_name] logging.debug("Data keys: %s", p_data.keys()) diff --git a/bin/oref0-autotune-export-to-xlsx.py b/bin/oref0-autotune-export-to-xlsx.py old mode 100755 new mode 100644 index 8dc19c33c..470bbb48f --- a/bin/oref0-autotune-export-to-xlsx.py +++ b/bin/oref0-autotune-export-to-xlsx.py @@ -28,20 +28,57 @@ import argparse import re + + def parseDateAndRun(filename): + """Parse date and run number from filename + + Parses the date and run number from a filename. + + Args: + filename (str): Filename (required) + + Returns: + tuple: (date, run) + """ + m=re.match( r'.*profile.(?P[0-9]*).(?P20[0-9][0-9]-[01][0-9]-[0-3][0-9]).json', filename) if m: return (m.group('date'), m.group('run')) else: # not found return ('0','0') + + def calc_minutes(timestr): + """Returns the number of minutes from midnight. Seconds are ignored + + Args: + timestr (str): Time string in format HH:MM + + Returns: + int: Number of minutes from midnight + """ + # returns the number of minutes from midnight. seconds are ignored # based on http://stackoverflow.com/questions/10663720/converting-a-time-string-to-seconds-in-python ftr = [60,1,0] # ignore seconds, count minutes, and use 60 minutes per hour return sum([a*b for a,b in zip(ftr, map(int,timestr.split(':')))]) + + def expandProfile(l, valueField, offsetField): + """Expand a profile to cover the whole day + + Args: + l (list): Profile list (required) + valueField (str): Value field (required) + offsetField (str): Offset field (required) + + Returns: + list: Expanded profile + """ + r=[] minutes=0 value=l[0][valueField] @@ -63,7 +100,22 @@ def expandProfile(l, valueField, offsetField): # return the expanded profile return r + + def writeExcelHeader(ws, date_format, headerFormat): + """Write header to Excel worksheet + + Writes the header to the Excel worksheet. + + Args: + ws (xlsxwriter.worksheet.Worksheet): Excel worksheet (required) + date_format (xlsxwriter.format.Format): Excel date format (required) + headerFormat (xlsxwriter.format.Format): Excel header format (required) + + Returns: + None + """ + ws.write_string(0,0, 'Filename', headerFormat) ws.write_string(0,1, 'Date', headerFormat) ws.write_string(0,2, 'Run', headerFormat) @@ -74,7 +126,23 @@ def writeExcelHeader(ws, date_format, headerFormat): ws.write_datetime(0, col, dt, date_format) col=col+1 + + def write_profile(worksheet, row, json, excel_number_format): + """Write profile to worksheet + + Writes a profile to a worksheet. + + Args: + worksheet (xlsxwriter.worksheet.Worksheet): Worksheet to write to + row (int): Row to write to + json (dict): Profile to write + excel_number_format (xlsxwriter.format.Format): Format to use for numbers + + Returns: + None + """ + worksheet.write_string(row, 0, filename) date, run = parseDateAndRun(filename) worksheet.write_string(row, 1, date) @@ -96,7 +164,20 @@ def write_timebased_profile(worksheet, row, expandedList, excel_number_format): worksheet.write_number(row, col, expandedList[i], excel_number_format) col=col+1 + + def excel_init_workbook(workbook): + """Initialize Excel workbook + + Initializes the Excel workbook with the formats that will be used in the rest of the program. + + Args: + workbook (Excel workbook): Excel workbook (required) + + Returns: + None + """ + #see http://xlsxwriter.readthedocs.io/format.html#format for documentation on the Excel format's excel_hour_format = workbook.add_format({'num_format': 'hh:mm', 'bold': True, 'font_color': 'black'}) excel_2decimals_format = workbook.add_format({'num_format': '0.00', 'font_size': '16'}) diff --git a/bin/oref0-autotune.py b/bin/oref0-autotune.py old mode 100755 new mode 100644 index c936641c1..f3d219217 --- a/bin/oref0-autotune.py +++ b/bin/oref0-autotune.py @@ -38,7 +38,20 @@ TERMINAL_LOGGING = True RECOMMENDS_REPORT = True + + def get_input_arguments(): + """Get command line arguments + + Gets the command line arguments, and then returns them to main. + + Args: + None + + Returns: + args (argparse.Namespace): Namespace object containing all of the command line arguments + """ + parser = argparse.ArgumentParser(description='Autotune') # Required @@ -82,7 +95,20 @@ def get_input_arguments(): return parser.parse_args() + + def assign_args_to_variables(args): + """Assign arguments to variables + + Takes the arguments passed to the script and assigns them to variables. + + Args: + args (argparse.Namespace): Arguments passed to the script + + Returns: + None + """ + # TODO: Input checking. global DIR, NIGHTSCOUT_HOST, START_DATE, END_DATE, NUMBER_OF_RUNS, \ @@ -108,13 +134,39 @@ def assign_args_to_variables(args): if args.log is not None: RECOMMENDS_REPORT = args.logs + + def get_nightscout_profile(nightscout_host): + """Get Nightscout profile from Nightscout host + + Gets the Nightscout profile from the Nightscout host, and then saves it to the autotune directory. + + Args: + nightscout_host (str): Nightscout URL (required) + + Returns: + None + """ + #TODO: Add ability to use API secret for Nightscout. res = requests.get(nightscout_host + '/api/v1/profile.json') with open(os.path.join(autotune_directory, 'nightscout.profile.json'), 'w') as f: # noqa: F821 f.write(res.text) + + def get_openaps_profile(directory): + """Get the current profile from openaps + + Copies the current profile from openaps into the autotune directory. + + Args: + directory (str): Path to autotune directory (required) + + Returns: + None + """ + shutil.copy(os.path.join(directory, 'settings', 'pumpprofile.json'), os.path.join(directory, 'autotune', 'profile.pump.json')) # If a previous valid settings/autotune.json exists, use that; otherwise start from settings/profile.json @@ -139,7 +191,23 @@ def get_openaps_profile(directory): #TODO: Do the correct copying here. # cat autotune/profile.json | json | grep -q start || cp autotune/profile.pump.json autotune/profile.json']) + + def get_nightscout_carb_and_insulin_treatments(nightscout_host, start_date, end_date, directory): + """Grab treatments.json from Nightscout + + Gets the treatments.json file from Nightscout, and saves it to the autotune directory. + + Args: + nightscout_host (str): Nightscout URL (required) + start_date (datetime): Start date (required) + end_date (datetime): End date (required) + directory (str): Autotune directory (required) + + Returns: + None + """ + logging.info('Grabbing NIGHTSCOUT treatments.json for date range: {0} to {1}'.format(start_date, end_date)) # TODO: What does 'T20:00-05:00' mean? output_file_name = os.path.join(directory, 'autotune', 'ns-treatments.json') @@ -151,7 +219,23 @@ def get_nightscout_carb_and_insulin_treatments(nightscout_host, start_date, end_ with open(output_file_name, 'w') as f: f.write(res.text.encode('utf-8')) + + def get_nightscout_bg_entries(nightscout_host, start_date, end_date, directory): + """Grab entries/sgv.json from nightscout for date range + + Gets the entries/sgv.json from nightscout for the specified date range. + + Args: + nightscout_host (str): Nightscout URL (required) + start_date (datetime): Start date (required) + end_date (datetime): End date (required) + directory (str): Directory to save files to (required) + + Returns: + None + """ + logging.info('Grabbing NIGHTSCOUT enries/sgv.json for date range: {0} to {1}'.format(start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))) date_list = [start_date + datetime.timedelta(days=x) for x in range(0, (end_date - start_date).days)] @@ -163,7 +247,23 @@ def get_nightscout_bg_entries(nightscout_host, start_date, end_date, directory): with open(os.path.join(directory, 'autotune', 'ns-entries.{date}.json'.format(date=date.strftime("%Y-%m-%d"))), 'w') as f: f.write(res.text.encode('utf-8')) + + def run_autotune(start_date, end_date, number_of_runs, directory): + """Run autotune for a given number of runs + + Runs autotune for a given number of runs, for a given date range. + + Args: + start_date (datetime): Start date of date range (required) + end_date (datetime): End date of date range (required) + number_of_runs (int): Number of runs (required) + directory (str): Directory to store autotune files (required) + + Returns: + None + """ + date_list = [start_date + datetime.timedelta(days=x) for x in range(0, (end_date - start_date).days)] autotune_directory = os.path.join(directory, 'autotune') for run_number in range(1, number_of_runs + 1): @@ -210,11 +310,39 @@ def run_autotune(start_date, end_date, number_of_runs, directory): shutil.copy(os.path.join(autotune_directory, 'newprofile.{run_number}.{date}.json'.format(run_number=run_number, date=date.strftime("%Y-%m-%d"))), os.path.join(autotune_directory, 'profile.json')) + + def export_to_excel(output_directory, output_excel_filename): + """Export autotune data to Excel + + Exports autotune data to Excel. + + Args: + output_directory (str): Output directory (required) + output_excel_filename (str): Output Excel filename (required) + + Returns: + None + """ + autotune_export_to_xlsx = 'oref0-autotune-export-to-xlsx --dir {0} --output {1}'.format(output_directory, output_excel_filename) call(autotune_export_to_xlsx, shell=True) + + def create_summary_report_and_display_results(output_directory): + """Create a summary report and display the results + + Creates a summary report of the autotune results, and then displays the results + to the terminal. + + Args: + output_directory (str): Output directory (required) + + Returns: + None + """ + print() print("Autotune pump profile recommendations:") print("---------------------------------------------------------") diff --git a/bin/oref0_autotune_export_to_xlsx.py b/bin/oref0_autotune_export_to_xlsx.py old mode 100755 new mode 100644 index 206470268..61e150cdd --- a/bin/oref0_autotune_export_to_xlsx.py +++ b/bin/oref0_autotune_export_to_xlsx.py @@ -28,20 +28,57 @@ import argparse import re + + def parseDateAndRun(filename): + """Parse date and run from filename + + Parses the date and run from a filename. + + Args: + filename (str): Filename (required) + + Returns: + tuple: (date, run) + """ + m=re.match( r'profile.(?P.*).(?P20[0-9][0-9]-[01][0-9]-[0-3][0-9]).json', filename) if m: return (m.group('date'), m.group('run')) else: # not found return ('-','-') + + def calc_minutes(timestr): + """Returns the number of minutes from midnight. Seconds are ignored + + Args: + timestr (str): Time string in HH:MM format (required) + + Returns: + int: Number of minutes from midnight + """ + # returns the number of minutes from midnight. seconds are ignored # based on http://stackoverflow.com/questions/10663720/converting-a-time-string-to-seconds-in-python ftr = [60,1,0] # ignore seconds, count minutes, and use 60 minutes per hour return sum([a*b for a,b in zip(ftr, map(int,timestr.split(':')))]) + + def expandProfile(l, valueField, offsetField): + """Expand a profile to cover the full day + + Args: + l (list): Profile list (required) + valueField (str): Value field (required) + offsetField (str): Offset field (required) + + Returns: + list: Expanded profile + """ + r=[] minutes=0 value=l[0][valueField] @@ -63,7 +100,22 @@ def expandProfile(l, valueField, offsetField): # return the expanded profile return r + + def writeExcelHeader(ws, date_format, headerFormat): + """Write header to Excel worksheet + + Writes the header to the Excel worksheet. + + Args: + ws (xlsxwriter.worksheet.Worksheet): Excel worksheet (required) + date_format (xlsxwriter.format.Format): Excel date format (required) + headerFormat (xlsxwriter.format.Format): Excel header format (required) + + Returns: + None + """ + ws.write_string(0,0, 'Filename', headerFormat) ws.write_string(0,1, 'Date', headerFormat) ws.write_string(0,2, 'Run', headerFormat) @@ -74,7 +126,23 @@ def writeExcelHeader(ws, date_format, headerFormat): ws.write_datetime(0, col, dt, date_format) col=col+1 + + def write_excel_profile(worksheet, row, expandedList, excel_number_format): + """Write profile to Excel worksheet + + Writes a profile to an Excel worksheet. + + Args: + worksheet (xlsxwriter.worksheet.Worksheet): Excel worksheet (required) + row (int): Row number (required) + expandedList (list): List of expanded profile entries (required) + excel_number_format (xlsxwriter.format.Format): Excel number format (required) + + Returns: + None + """ + worksheet.write_string(row, 0, filename) date, run = parseDateAndRun(filename) worksheet.write_string(row, 1, date) @@ -84,7 +152,20 @@ def write_excel_profile(worksheet, row, expandedList, excel_number_format): worksheet.write_number(row, col, expandedList[i], excel_number_format) col=col+1 + + def excel_init_workbook(workbook): + """Initialize the Excel workbook + + Initializes the Excel workbook, and returns the worksheet objects for the basal and isf profiles. + + Args: + workbook (xlsxwriter.Workbook): Workbook object (required) + + Returns: + (xlsxwriter.Worksheet, xlsxwriter.Worksheet): Worksheet objects for basal and isf profiles + """ + #see http://xlsxwriter.readthedocs.io/format.html#format for documentation on the Excel format's excel_hour_format = workbook.add_format({'num_format': 'hh:mm', 'bold': True, 'font_color': 'black'}) excel_2decimals_format = workbook.add_format({'num_format': '0.00', 'font_size': '16'}) diff --git a/bin/oref0_nightscout_check.py b/bin/oref0_nightscout_check.py old mode 100755 new mode 100644 index 90b6edcc2..256212a4e --- a/bin/oref0_nightscout_check.py +++ b/bin/oref0_nightscout_check.py @@ -21,13 +21,39 @@ token_dict["exp"]=-1 auth_headers={} + + def init(args): + """Initialize logging + + Sets up logging, and sets the log level based on the verbose flag. + + Args: + args (argparse.Namespace): Arguments passed to the script + + Returns: + None + """ + if args.verbose: logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format='%(asctime)s %(levelname)s %(message)s') else: logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(asctime)s %(levelname)s %(message)s') + + def parse_ns_ini(filename): + """Parse Nightscout ini file + + Parses the Nightscout ini file, and sets the global variables nightscout_host, api_secret, and token_secret. + + Args: + filename (str): Nightscout ini file (required) + + Returns: + None + """ + global nightscout_host, api_secret, token_secret logging.debug("Parsing %s" % filename) config = configparser.ConfigParser() @@ -64,7 +90,19 @@ def parse_ns_ini(filename): sys.exit(1) + def get_nightscout_authorization_token(): + """Get Nightscout authorization token + + Gets an authorization token from Nightscout. + + Args: + None + + Returns: + None + """ + global nightscout_host, token_secret, token_dict, auth_headers logging.debug("get_nightscout_authorization_token") try: @@ -85,12 +123,38 @@ def get_nightscout_authorization_token(): sys.exit(1) + def startup_checks(args): + """Perform startup checks + + Checks for the existence of the nightscout.ini file, and then checks + the nightscout host and token. + + Args: + args (argparse.Namespace): Arguments passed to the script + + Returns: + None + """ + parse_ns_ini(args.nsini) logging.info("Nightscout host: %s" % nightscout_host) get_nightscout_authorization_token() + + def check_permissions(): + """Check if the token has the required permissions + + Checks if the token has the required permissions to use the API. + + Args: + None + + Returns: + None + """ + global token_dict pg=token_dict['permissionGroups'][0] if pg==["*"]: # admin role