Merge pull request #324 from aceinnolab/feature/#322
Add support for longer update intervals
This commit is contained in:
		| @@ -21,7 +21,7 @@ logs = logging.getLogger(__name__) | |||||||
| logs.setLevel(level=logging.INFO) | logs.setLevel(level=logging.INFO) | ||||||
|  |  | ||||||
| # Get the path to the Inkycal folder | # 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 | # Get path of 'fonts' and 'images' folders within Inkycal folder | ||||||
| fonts_location = os.path.join(top_level, "fonts/") | fonts_location = os.path.join(top_level, "fonts/") | ||||||
|   | |||||||
| @@ -2,15 +2,14 @@ | |||||||
| Inkycal ePaper driving functions | Inkycal ePaper driving functions | ||||||
| Copyright by aceisace | Copyright by aceisace | ||||||
| """ | """ | ||||||
| import logging |  | ||||||
| import os | import os | ||||||
| import traceback |  | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
|  |  | ||||||
| import PIL | import PIL | ||||||
| from PIL import Image | from PIL import Image | ||||||
|  |  | ||||||
| from inkycal.custom import top_level | from inkycal.custom import top_level | ||||||
|  | from inkycal.display.supported_models import supported_models | ||||||
|  |  | ||||||
|  |  | ||||||
| def import_driver(model): | def import_driver(model): | ||||||
| @@ -47,14 +46,12 @@ class Display: | |||||||
|         except FileNotFoundError: |         except FileNotFoundError: | ||||||
|             raise Exception('SPI could not be found. Please check if SPI is enabled') |             raise Exception('SPI could not be found. Please check if SPI is enabled') | ||||||
|  |  | ||||||
|  |  | ||||||
|     def test(self) -> None: |     def test(self) -> None: | ||||||
|         """Test the display by showing a test image""" |         """Test the display by showing a test image""" | ||||||
|         # TODO implement test image |         # TODO implement test image | ||||||
|         raise NotImplementedError("Devs were too lazy again, sorry, please try again later") |         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. |         """Renders an image on the selected E-Paper display. | ||||||
|  |  | ||||||
|         Initlializes the E-Paper display, sends image data and executes command |         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): |     def get_display_size(cls, model_name) -> (int, int): | ||||||
|         """Returns the size of the display as a tuple -> (width, height) |         """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. |         size. | ||||||
|  |  | ||||||
|         Args: |         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: |         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: |         You can use this function directly without creating the Display class: | ||||||
|  |  | ||||||
|         >>> Display.get_display_size('model_name') |         >>> Display.get_display_size('model_name') | ||||||
|         """ |         """ | ||||||
|         try: |         if model_name in supported_models: | ||||||
|             driver = import_driver(model_name) |             return supported_models[model_name] | ||||||
|             return driver.EPD_WIDTH, driver.EPD_HEIGHT |         raise AssertionError(f'{model_name} not found in supported models') | ||||||
|         except: |  | ||||||
|             logging.error(f'Failed to load driver for ${model_name}. Check spelling?') |  | ||||||
|             print(traceback.format_exc()) |  | ||||||
|             raise AssertionError("Could not import driver") |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_display_names(cls) -> list: |     def get_display_names(cls) -> list: | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								inkycal/display/supported_models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								inkycal/display/supported_models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | } | ||||||
| @@ -3,28 +3,22 @@ Main class for inkycal Project | |||||||
| Copyright by aceinnolab | Copyright by aceinnolab | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
| import glob | import glob | ||||||
| import hashlib | import hashlib | ||||||
| import json |  | ||||||
| from logging.handlers import RotatingFileHandler | from logging.handlers import RotatingFileHandler | ||||||
|  |  | ||||||
| import arrow |  | ||||||
| import numpy | import numpy | ||||||
| import asyncio |  | ||||||
|  |  | ||||||
|  |  | ||||||
| from inkycal.custom import * | from inkycal.custom import * | ||||||
| from inkycal.display import Display | from inkycal.display import Display | ||||||
| from inkycal.modules.inky_image import Inkyimage as Images | from inkycal.modules.inky_image import Inkyimage as Images | ||||||
|  |  | ||||||
| from PIL import Image |  | ||||||
|  |  | ||||||
| # On the console, set a logger to show only important logs | # On the console, set a logger to show only important logs | ||||||
| # (level ERROR or higher) | # (level ERROR or higher) | ||||||
| stream_handler = logging.StreamHandler() | stream_handler = logging.StreamHandler() | ||||||
| stream_handler.setLevel(logging.ERROR) | stream_handler.setLevel(logging.ERROR) | ||||||
|  |  | ||||||
|  |  | ||||||
| if not os.path.exists(f'{top_level}/logs'): | if not os.path.exists(f'{top_level}/logs'): | ||||||
|     os.mkdir(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. |         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""" |         """Initialise Inkycal""" | ||||||
|  |  | ||||||
|         # Get the release version from setup.py |         # Get the release version from setup.py | ||||||
| @@ -87,7 +81,8 @@ class Inkycal: | |||||||
|                     self.settings = settings |                     self.settings = settings | ||||||
|  |  | ||||||
|             except FileNotFoundError: |             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: |         else: | ||||||
|             try: |             try: | ||||||
| @@ -108,6 +103,8 @@ class Inkycal: | |||||||
|  |  | ||||||
|         self.show_border = self.settings.get('border_around_modules', False) |         self.show_border = self.settings.get('border_around_modules', False) | ||||||
|  |  | ||||||
|  |         self.cleanup() | ||||||
|  |  | ||||||
|         # Load drivers if image should be rendered |         # Load drivers if image should be rendered | ||||||
|         if self.render: |         if self.render: | ||||||
|             # Init Display class with model in settings file |             # 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') |                 logger.exception(f'Could not find module: "{module}". Please try to import manually') | ||||||
|  |  | ||||||
|             # If something unexpected happened, show the error message |             # If something unexpected happened, show the error message | ||||||
|             except Exception as e: |             except: | ||||||
|                 logger.exception(f"Exception: {traceback.format_exc()}.") |                 logger.exception(f"Exception: {traceback.format_exc()}.") | ||||||
|  |  | ||||||
|         # Path to store images |         # Path to store images | ||||||
| @@ -158,8 +155,16 @@ class Inkycal: | |||||||
|         # Give an OK message |         # Give an OK message | ||||||
|         print('loaded inkycal') |         print('loaded inkycal') | ||||||
|  |  | ||||||
|     def countdown(self, interval_mins=None): |     def countdown(self, interval_mins: int or None = None) -> int: | ||||||
|         """Returns the remaining time in seconds until next display update""" |         """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 |         # Check if empty, if empty, use value from settings file | ||||||
|         if interval_mins is None: |         if interval_mins is None: | ||||||
| @@ -167,20 +172,30 @@ class Inkycal: | |||||||
|  |  | ||||||
|         # Find out at which minutes the update should happen |         # Find out at which minutes the update should happen | ||||||
|         now = arrow.now() |         now = arrow.now() | ||||||
|         update_timings = [(60 - int(interval_mins) * updates) for updates in |         if interval_mins <= 60: | ||||||
|                           range(60 // int(interval_mins))][::-1] |             update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1] | ||||||
|  |  | ||||||
|         # Calculate time in minutes until next update |             # Calculate time in minutes until next update | ||||||
|         minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute |             minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute | ||||||
|  |  | ||||||
|         # Print the remaining time in minutes until next update |             # Print the remaining time in minutes until next update | ||||||
|         print(f'{minutes} minutes left until next refresh') |             print(f'{minutes} minutes left until next refresh') | ||||||
|  |  | ||||||
|         # Calculate time in seconds until next update |             # Calculate time in seconds until next update | ||||||
|         remaining_time = minutes * 60 + (60 - now.second) |             remaining_time = minutes * 60 + (60 - now.second) | ||||||
|  |  | ||||||
|         # Return seconds until next update |             # Return seconds until next update | ||||||
|         return remaining_time |             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): |     def test(self): | ||||||
|         """Tests if Inkycal can run without issues. |         """Tests if Inkycal can run without issues. | ||||||
| @@ -262,7 +277,6 @@ class Inkycal: | |||||||
|             print("Refresh needed: {a}".format(a=res)) |             print("Refresh needed: {a}".format(a=res)) | ||||||
|         return res |         return res | ||||||
|  |  | ||||||
|  |  | ||||||
|     async def run(self): |     async def run(self): | ||||||
|         """Runs main program in nonstop mode. |         """Runs main program in nonstop mode. | ||||||
|  |  | ||||||
| @@ -346,8 +360,8 @@ class Inkycal: | |||||||
|  |  | ||||||
|                     # render the image on the display |                     # render the image on the display | ||||||
|                     if not self.settings.get('image_hash', False) or self._needs_image_update([ |                     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), | ||||||
|                       (f"{self.image_folder}/canvas_colour.png.hash", im_colour) |                         (f"{self.image_folder}/canvas_colour.png.hash", im_colour) | ||||||
|                     ]): |                     ]): | ||||||
|                         # render the image on the display |                         # render the image on the display | ||||||
|                         display.render(im_black, im_colour) |                         display.render(im_black, im_colour) | ||||||
| @@ -362,7 +376,7 @@ class Inkycal: | |||||||
|                         im_black = upside_down(im_black) |                         im_black = upside_down(im_black) | ||||||
|  |  | ||||||
|                     if not self.settings.get('image_hash', False) or self._needs_image_update([ |                     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) |                         display.render(im_black) | ||||||
|  |  | ||||||
| @@ -557,6 +571,16 @@ class Inkycal: | |||||||
|         else: |         else: | ||||||
|             self._calibration_state = False |             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__': | if __name__ == '__main__': | ||||||
|     print(f'running inkycal main in standalone/debug mode') |     print(f'running inkycal main in standalone/debug mode') | ||||||
|   | |||||||
| @@ -17,12 +17,35 @@ class TestMain(unittest.TestCase): | |||||||
|         assert inkycal.settings["model"] == "image_file" |         assert inkycal.settings["model"] == "image_file" | ||||||
|         assert inkycal.settings["update_interval"] == 5 |         assert inkycal.settings["update_interval"] == 5 | ||||||
|         assert inkycal.settings["orientation"] == 0 |         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["info_section_height"] == 70 | ||||||
|         assert inkycal.settings["border_around_modules"] == True |         assert inkycal.settings["border_around_modules"] is True | ||||||
|  |  | ||||||
|     def test_run(self): |     def test_run(self): | ||||||
|         inkycal = Inkycal(self.settings_path, render=False) |         inkycal = Inkycal(self.settings_path, render=False) | ||||||
|         inkycal.test() |         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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user