diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index f0c9356..327095c 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -21,7 +21,7 @@ logs = logging.getLogger(__name__) logs.setLevel(level=logging.INFO) # Get the path to the Inkycal folder -top_level = os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/inkycal")[0] +top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1]) # Get path of 'fonts' and 'images' folders within Inkycal folder fonts_location = os.path.join(top_level, "fonts/") diff --git a/inkycal/display/display.py b/inkycal/display/display.py index 5fb6420..89fdf4c 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -2,15 +2,14 @@ Inkycal ePaper driving functions Copyright by aceisace """ -import logging import os -import traceback from importlib import import_module import PIL from PIL import Image from inkycal.custom import top_level +from inkycal.display.supported_models import supported_models def import_driver(model): @@ -47,14 +46,12 @@ class Display: except FileNotFoundError: raise Exception('SPI could not be found. Please check if SPI is enabled') - def test(self) -> None: """Test the display by showing a test image""" # TODO implement test image raise NotImplementedError("Devs were too lazy again, sorry, please try again later") - - def render(self, im_black: PIL.Image, im_colour: PIL.Image or None=None) -> None: + def render(self, im_black: PIL.Image, im_colour: PIL.Image or None = None) -> None: """Renders an image on the selected E-Paper display. Initlializes the E-Paper display, sends image data and executes command @@ -166,26 +163,25 @@ class Display: def get_display_size(cls, model_name) -> (int, int): """Returns the size of the display as a tuple -> (width, height) - Looks inside "drivers" folder for the given model name, then returns it's + Looks inside supported_models file for the given model name, then returns it's size. Args: - - model_name: str -> The name of the E-Paper display to get it's size. + model_name: str -> The name of the E-Paper display to get it's size. Returns: - (width, height) ->tuple, showing the size of the display + (width, height) representing the size of the display + + Raises: + AssertionError: If the display name was not found in the supported models. You can use this function directly without creating the Display class: >>> Display.get_display_size('model_name') """ - try: - driver = import_driver(model_name) - return driver.EPD_WIDTH, driver.EPD_HEIGHT - except: - logging.error(f'Failed to load driver for ${model_name}. Check spelling?') - print(traceback.format_exc()) - raise AssertionError("Could not import driver") + if model_name in supported_models: + return supported_models[model_name] + raise AssertionError(f'{model_name} not found in supported models') @classmethod def get_display_names(cls) -> list: diff --git a/inkycal/display/supported_models.py b/inkycal/display/supported_models.py new file mode 100644 index 0000000..27d4d2a --- /dev/null +++ b/inkycal/display/supported_models.py @@ -0,0 +1,19 @@ +supported_models = { + 'epd_12_in_48': (1304, 984), + 'epd_7_in_5_colour': (640, 384), + '9_in_7': (1200, 825), + 'epd_5_in_83_colour': (600, 448), + 'epd_12_in_48_colour': (1304, 984), + 'epd_4_in_2_colour': (400, 300), + 'epd_7_in_5_v2': (800, 480), + 'epd_12_in_48_colour_V2': (1304, 984), + 'epd_7_in_5': (640, 384), + 'epd5in83b_V2': (648, 480), + 'epd_7_in_5_v3': (880, 528), + '10_in_3': (1872, 1404), + 'epd_7_in_5_v2_colour': (800, 480), + 'epd_4_in_2': (400, 300), + '7_in_8': (1872, 1404), + 'epd_7_in_5_v3_colour': (880, 528), + 'epd_5_in_83': (600, 448) +} diff --git a/inkycal/main.py b/inkycal/main.py index 69758a9..cc4473b 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -3,28 +3,22 @@ Main class for inkycal Project Copyright by aceinnolab """ +import asyncio import glob import hashlib -import json from logging.handlers import RotatingFileHandler -import arrow import numpy -import asyncio - from inkycal.custom import * from inkycal.display import Display from inkycal.modules.inky_image import Inkyimage as Images -from PIL import Image - # On the console, set a logger to show only important logs # (level ERROR or higher) stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.ERROR) - if not os.path.exists(f'{top_level}/logs'): os.mkdir(f'{top_level}/logs') @@ -66,7 +60,7 @@ class Inkycal: to improve rendering on E-Papers. Set this to False for 9.7" E-Paper. """ - def __init__(self, settings_path:str or None=None, render:bool=True): + def __init__(self, settings_path: str or None = None, render: bool = True): """Initialise Inkycal""" # Get the release version from setup.py @@ -87,7 +81,8 @@ class Inkycal: self.settings = settings except FileNotFoundError: - raise FileNotFoundError(f"No settings.json file could be found in the specified location: {settings_path}") + raise FileNotFoundError( + f"No settings.json file could be found in the specified location: {settings_path}") else: try: @@ -108,6 +103,8 @@ class Inkycal: self.show_border = self.settings.get('border_around_modules', False) + self.cleanup() + # Load drivers if image should be rendered if self.render: # Init Display class with model in settings file @@ -146,7 +143,7 @@ class Inkycal: logger.exception(f'Could not find module: "{module}". Please try to import manually') # If something unexpected happened, show the error message - except Exception as e: + except: logger.exception(f"Exception: {traceback.format_exc()}.") # Path to store images @@ -158,8 +155,16 @@ class Inkycal: # Give an OK message print('loaded inkycal') - def countdown(self, interval_mins=None): - """Returns the remaining time in seconds until next display update""" + def countdown(self, interval_mins: int or None = None) -> int: + """Returns the remaining time in seconds until next display update. + + Args: + - interval_mins = int -> the interval in minutes for the update + if no interval is given, the value from the settings file is used. + + Returns: + - int -> the remaining time in seconds until next update + """ # Check if empty, if empty, use value from settings file if interval_mins is None: @@ -167,20 +172,30 @@ class Inkycal: # Find out at which minutes the update should happen now = arrow.now() - update_timings = [(60 - int(interval_mins) * updates) for updates in - range(60 // int(interval_mins))][::-1] + if interval_mins <= 60: + update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1] - # Calculate time in minutes until next update - minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute + # Calculate time in minutes until next update + minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute - # Print the remaining time in minutes until next update - print(f'{minutes} minutes left until next refresh') + # Print the remaining time in minutes until next update + print(f'{minutes} minutes left until next refresh') - # Calculate time in seconds until next update - remaining_time = minutes * 60 + (60 - now.second) + # Calculate time in seconds until next update + remaining_time = minutes * 60 + (60 - now.second) - # Return seconds until next update - return remaining_time + # Return seconds until next update + return remaining_time + else: + # Calculate time in minutes until next update using the range of 24 hours in steps of every full hour + update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1] + minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute + remaining_time = minutes * 60 + (60 - now.second) + + print(f'{round(minutes / 60, 1)} hours left until next refresh') + + # Return seconds until next update + return remaining_time def test(self): """Tests if Inkycal can run without issues. @@ -262,7 +277,6 @@ class Inkycal: print("Refresh needed: {a}".format(a=res)) return res - async def run(self): """Runs main program in nonstop mode. @@ -346,8 +360,8 @@ class Inkycal: # render the image on the display if not self.settings.get('image_hash', False) or self._needs_image_update([ - (f"{self.image_folder}/canvas.png.hash", im_black), - (f"{self.image_folder}/canvas_colour.png.hash", im_colour) + (f"{self.image_folder}/canvas.png.hash", im_black), + (f"{self.image_folder}/canvas_colour.png.hash", im_colour) ]): # render the image on the display display.render(im_black, im_colour) @@ -362,7 +376,7 @@ class Inkycal: im_black = upside_down(im_black) if not self.settings.get('image_hash', False) or self._needs_image_update([ - (f"{self.image_folder}/canvas.png.hash", im_black), + (f"{self.image_folder}/canvas.png.hash", im_black), ]): display.render(im_black) @@ -557,6 +571,16 @@ class Inkycal: else: self._calibration_state = False + @staticmethod + def cleanup(): + # clean up old images in image_folder + for _file in glob.glob(f"{image_folder}*.png"): + try: + os.remove(_file) + except: + logger.error(f"could not remove file: {_file}") + pass + if __name__ == '__main__': print(f'running inkycal main in standalone/debug mode') diff --git a/tests/test_main.py b/tests/test_main.py index 9cacfab..ceb834c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -17,12 +17,35 @@ class TestMain(unittest.TestCase): assert inkycal.settings["model"] == "image_file" assert inkycal.settings["update_interval"] == 5 assert inkycal.settings["orientation"] == 0 - assert inkycal.settings["info_section"] == True + assert inkycal.settings["info_section"] is True assert inkycal.settings["info_section_height"] == 70 - assert inkycal.settings["border_around_modules"] == True + assert inkycal.settings["border_around_modules"] is True def test_run(self): inkycal = Inkycal(self.settings_path, render=False) inkycal.test() + def test_countdown(self): + inkycal = Inkycal(self.settings_path, render=False) + remaining_time = inkycal.countdown(5) + assert 1 <= remaining_time <= 5 * 60 + remaining_time = inkycal.countdown(10) + assert 1 <= remaining_time <= 10 * 60 + remaining_time = inkycal.countdown(15) + assert 1 <= remaining_time <= 15 * 60 + remaining_time = inkycal.countdown(20) + assert 1 <= remaining_time <= 20 * 60 + remaining_time = inkycal.countdown(30) + assert 1 <= remaining_time <= 30 * 60 + remaining_time = inkycal.countdown(60) + assert 1 <= remaining_time <= 60 * 60 + + remaining_time = inkycal.countdown(120) + assert 1 <= remaining_time <= 120 * 2 * 60 + remaining_time = inkycal.countdown(240) + assert 1 <= remaining_time <= 240 * 2 * 60 + remaining_time = inkycal.countdown(600) + assert 1 <= remaining_time <= 600 * 2 * 60 + remaining_time = inkycal.countdown(1200) + assert 1 <= remaining_time <= 1200 * 2 * 60