diff --git a/icons/ui-icons/home_temp.png b/fonts/ui-icons/home_temp.png similarity index 100% rename from icons/ui-icons/home_temp.png rename to fonts/ui-icons/home_temp.png diff --git a/icons/ui-icons/humidity.bmp b/fonts/ui-icons/humidity.bmp similarity index 100% rename from icons/ui-icons/humidity.bmp rename to fonts/ui-icons/humidity.bmp diff --git a/icons/ui-icons/outline_thermostat_white_48dp.bmp b/fonts/ui-icons/outline_thermostat_white_48dp.bmp similarity index 100% rename from icons/ui-icons/outline_thermostat_white_48dp.bmp rename to fonts/ui-icons/outline_thermostat_white_48dp.bmp diff --git a/icons/ui-icons/rain-chance.bmp b/fonts/ui-icons/rain-chance.bmp similarity index 100% rename from icons/ui-icons/rain-chance.bmp rename to fonts/ui-icons/rain-chance.bmp diff --git a/icons/ui-icons/uv.bmp b/fonts/ui-icons/uv.bmp similarity index 100% rename from icons/ui-icons/uv.bmp rename to fonts/ui-icons/uv.bmp diff --git a/icons/ui-icons/wind.bmp b/fonts/ui-icons/wind.bmp similarity index 100% rename from icons/ui-icons/wind.bmp rename to fonts/ui-icons/wind.bmp diff --git a/inkycal/__init__.py b/inkycal/__init__.py index 6c721f5..edb3926 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -1,20 +1,15 @@ -# Display class (for driving E-Paper displays) -from inkycal.display import Display - # Default modules import inkycal.modules.inkycal_agenda import inkycal.modules.inkycal_calendar -import inkycal.modules.inkycal_weather import inkycal.modules.inkycal_feeds -import inkycal.modules.inkycal_todoist +import inkycal.modules.inkycal_fullweather import inkycal.modules.inkycal_image import inkycal.modules.inkycal_jokes import inkycal.modules.inkycal_slideshow import inkycal.modules.inkycal_stocks +import inkycal.modules.inkycal_todoist +import inkycal.modules.inkycal_weather import inkycal.modules.inkycal_webshot import inkycal.modules.inkycal_xkcd -import inkycal.modules.inkycal_fullweather -import inkycal.modules.inkycal_mawaqit - -# Main file +from inkycal.display import Display from inkycal.main import Inkycal diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index 327095c..206a2ec 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -17,20 +17,16 @@ from PIL import Image from PIL import ImageDraw from PIL import ImageFont -logs = logging.getLogger(__name__) -logs.setLevel(level=logging.INFO) +from inkycal.settings import Settings -# Get the path to the Inkycal folder -top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1]) +logger = logging.getLogger(__name__) -# Get path of 'fonts' and 'images' folders within Inkycal folder -fonts_location = os.path.join(top_level, "fonts/") -image_folder = os.path.join(top_level, "image_folder/") +settings = Settings() # Get available fonts within fonts folder fonts = {} -for path, dirs, files in os.walk(fonts_location): +for path, dirs, files in os.walk(settings.FONT_PATH): for _ in files: if _.endswith(".otf"): name = _.split(".otf")[0] @@ -39,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location): if _.endswith(".ttf"): name = _.split(".ttf")[0] fonts[name] = os.path.join(path, _) -logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}") +logger.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}") available_fonts = [key for key, values in fonts.items()] @@ -81,12 +77,12 @@ def get_system_tz() -> str: """ try: local_tz = tzlocal.get_localzone().key - logs.debug(f"Local system timezone is {local_tz}.") + logger.debug(f"Local system timezone is {local_tz}.") except: - logs.error("System timezone could not be parsed!") - logs.error("Please set timezone manually!. Falling back to UTC...") + logger.error("System timezone could not be parsed!") + logger.error("Please set timezone manually!. Falling back to UTC...") local_tz = "UTC" - logs.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.") + logger.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.") return local_tz @@ -182,14 +178,14 @@ def write(image, xy, box_size, text, font=None, **kwargs): # Truncate text if text is too long, so it can fit inside the box if (text_width, text_height) > (box_width, box_height): - logs.debug(("truncating {}".format(text))) + logger.debug(("truncating {}".format(text))) while (text_width, text_height) > (box_width, box_height): text = text[0:-1] text_bbox = font.getbbox(text) text_width = text_bbox[2] - text_bbox[0] text_bbox_height = font.getbbox("hg") text_height = text_bbox_height[3] - text_bbox_height[1] - logs.debug(text) + logger.debug(text) # Align text to desired position if alignment == "center" or None: diff --git a/inkycal/display/drivers/10_in_3.py b/inkycal/display/drivers/10_in_3.py index 834e2df..7d7b3fa 100644 --- a/inkycal/display/drivers/10_in_3.py +++ b/inkycal/display/drivers/10_in_3.py @@ -2,22 +2,18 @@ 10.3" driver class Copyright by aceinnolab """ +import os from subprocess import run from PIL import Image -from inkycal.custom import image_folder, top_level +from inkycal.settings import Settings # Display resolution EPD_WIDTH = 1872 EPD_HEIGHT = 1404 -# Please insert VCOM of your display. The Minus sign before is not required -VCOM = "2.0" - -driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/' - -command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' +settings = Settings() class EPD: @@ -40,8 +36,8 @@ class EPD: def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT) - image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') - command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' + image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP') + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}' print(command) return command diff --git a/inkycal/display/drivers/7_in_8.py b/inkycal/display/drivers/7_in_8.py index fe4d243..5f9cf51 100644 --- a/inkycal/display/drivers/7_in_8.py +++ b/inkycal/display/drivers/7_in_8.py @@ -2,20 +2,16 @@ 7.8" parallel driver class Copyright by aceinnolab """ +import os from subprocess import run -from inkycal.custom import image_folder, top_level +from inkycal.settings import Settings # Display resolution EPD_WIDTH = 1872 EPD_HEIGHT = 1404 -# Please insert VCOM of your display. The Minus sign before is not required -VCOM = "2.0" - -driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/' - -command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' +settings = Settings() class EPD: @@ -38,8 +34,8 @@ class EPD: def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True) - image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') - command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' + image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP') + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}' print(command) return command diff --git a/inkycal/display/drivers/9_in_7.py b/inkycal/display/drivers/9_in_7.py index 8c6ec0f..c81feef 100644 --- a/inkycal/display/drivers/9_in_7.py +++ b/inkycal/display/drivers/9_in_7.py @@ -4,18 +4,16 @@ Copyright by aceinnolab """ from subprocess import run -from inkycal.custom import image_folder, top_level +from inkycal.settings import Settings # Display resolution EPD_WIDTH = 1200 EPD_HEIGHT = 825 -# Please insert VCOM of your display. The Minus sign before is not required -VCOM = "2.0" -driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/' +settings = Settings() -command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' +command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}' class EPD: @@ -38,8 +36,8 @@ class EPD: def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True) - image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') - command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}' + image.convert('RGB').save(settings.IMAGE_FOLDER + 'canvas.bmp', 'BMP') + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}' print(command) return command diff --git a/inkycal/loggers.py b/inkycal/loggers.py new file mode 100644 index 0000000..97c1c46 --- /dev/null +++ b/inkycal/loggers.py @@ -0,0 +1,35 @@ +"""Logging configuration for Inkycal.""" +import logging +import os +from logging.handlers import RotatingFileHandler + +from inkycal.settings import Settings + +# On the console, set a logger to show only important logs +# (level ERROR or higher) +stream_handler = logging.StreamHandler() +stream_handler.setLevel(logging.ERROR) + +settings = Settings() + +if not os.path.exists(settings.LOG_PATH): + os.mkdir(settings.LOG_PATH) + + +# Save all logs to a file, which contains more detailed output +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s | %(name)s | %(levelname)s: %(message)s', + datefmt='%d-%m-%Y %H:%M:%S', + handlers=[ + stream_handler, # add stream handler from above + RotatingFileHandler( # log to a file too + settings.INKYCAL_LOG_PATH, # file to log + maxBytes=2*1024*1024, # 2MB max filesize + backupCount=5 # create max 5 log files + ) + ] +) + +# Show less logging for PIL module +logging.getLogger("PIL").setLevel(logging.WARNING) \ No newline at end of file diff --git a/inkycal/main.py b/inkycal/main.py index cc4473b..ac3dd88 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -6,44 +6,21 @@ Copyright by aceinnolab import asyncio import glob import hashlib -from logging.handlers import RotatingFileHandler import numpy +from inkycal import loggers # noqa from inkycal.custom import * from inkycal.display import Display from inkycal.modules.inky_image import Inkyimage as Images - -# 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') - -# Save all logs to a file, which contains more detailed output -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s | %(name)s | %(levelname)s: %(message)s', - datefmt='%d-%m-%Y %H:%M:%S', - handlers=[ - stream_handler, # add stream handler from above - RotatingFileHandler( # log to a file too - f'{top_level}/logs/inkycal.log', # file to log - maxBytes=2097152, # 2MB max filesize - backupCount=5 # create max 5 log files - ) - ] -) - -# Show less logging for PIL module -logging.getLogger("PIL").setLevel(logging.WARNING) +from inkycal.utils.json_cache import JSONCache logger = logging.getLogger(__name__) +settings = Settings() + +CACHE_NAME = "inkycal_main" -# TODO: autostart -> supervisor? class Inkycal: """Inkycal main class @@ -62,13 +39,7 @@ class Inkycal: def __init__(self, settings_path: str or None = None, render: bool = True): """Initialise Inkycal""" - - # Get the release version from setup.py - with open(f'{top_level}/setup.py') as setup_file: - for line in setup_file: - if line.startswith('__version__'): - self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "") - break + self._release = "2.0.3" self.render = render self.info = None @@ -77,8 +48,7 @@ class Inkycal: if settings_path: try: with open(settings_path) as settings_file: - settings = json.load(settings_file) - self.settings = settings + self.settings = json.load(settings_file) except FileNotFoundError: raise FileNotFoundError( @@ -86,17 +56,19 @@ class Inkycal: else: try: - with open('/boot/settings.json') as settings_file: - settings = json.load(settings_file) - self.settings = settings + with open('/boot/settings.json', mode="r") as settings_file: + self.settings = json.load(settings_file) except FileNotFoundError: raise SettingsFileNotFoundError self.disable_calibration = self.settings.get('disable_calibration', False) - if not os.path.exists(image_folder): - os.mkdir(image_folder) + if not os.path.exists(settings.IMAGE_FOLDER): + os.mkdir(settings.IMAGE_FOLDER) + + if not os.path.exists(settings.CACHE_PATH): + os.mkdir(settings.CACHE_PATH) # Option to use epaper image optimisation, reduces colours self.optimize = True @@ -122,7 +94,7 @@ class Inkycal: # Load and initialise modules specified in the settings file self._module_number = 1 - for module in settings['modules']: + for module in self.settings['modules']: module_name = module['name'] try: loader = f'from inkycal.modules import {module_name}' @@ -131,10 +103,9 @@ class Inkycal: setup = f'self.module_{self._module_number} = {module_name}({module})' # print(setup) exec(setup) - logger.info(('name : {name} size : {width}x{height} px'.format( - name=module_name, - width=module['config']['size'][0], - height=module['config']['size'][1]))) + width = module['config']['size'][0] + height = module['config']['size'][1] + logger.info(f'name : {module_name} size : {width}x{height} px') self._module_number += 1 @@ -147,55 +118,56 @@ class Inkycal: logger.exception(f"Exception: {traceback.format_exc()}.") # Path to store images - self.image_folder = image_folder + self.image_folder = settings.IMAGE_FOLDER # Remove old hashes self._remove_hashes(self.image_folder) + # set up cache + if not os.path.exists(os.path.join(settings.CACHE_PATH, CACHE_NAME)): + if not os.path.exists(settings.CACHE_PATH): + os.mkdir(settings.CACHE_PATH) + self.cache = JSONCache(CACHE_NAME) + self.cache_data = self.cache.read() + # Give an OK message print('loaded inkycal') - def countdown(self, interval_mins: int or None = None) -> int: - """Returns the remaining time in seconds until next display update. + def countdown(self, interval_mins: int = None) -> int: + """Returns the remaining time in seconds until the next display update based on the interval. Args: - - interval_mins = int -> the interval in minutes for the update - if no interval is given, the value from the settings file is used. + interval_mins (int): The interval in minutes for the update. If none is given, the value + from the settings file is used. Returns: - - int -> the remaining time in seconds until next update + int: The remaining time in seconds until the next update. """ - - # Check if empty, if empty, use value from settings file + # Default to settings if no interval is provided if interval_mins is None: interval_mins = self.settings["update_interval"] - # Find out at which minutes the update should happen + # Get the current time now = arrow.now() - 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 the next update time + # Finding the total minutes from the start of the day + minutes_since_midnight = now.hour * 60 + now.minute - # Print the remaining time in minutes until next update - print(f'{minutes} minutes left until next refresh') + # Finding the next interval point + minutes_to_next_interval = ( + minutes_since_midnight // interval_mins + 1) * interval_mins - minutes_since_midnight + seconds_to_next_interval = minutes_to_next_interval * 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 + # Logging the remaining time in appropriate units + hours_to_next_interval = minutes_to_next_interval // 60 + remaining_minutes = minutes_to_next_interval % 60 + if hours_to_next_interval > 0: + print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh') 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'{remaining_minutes} minutes left until next refresh') - print(f'{round(minutes / 60, 1)} hours left until next refresh') - - # Return seconds until next update - return remaining_time + return seconds_to_next_interval def test(self): """Tests if Inkycal can run without issues. @@ -218,20 +190,13 @@ class Inkycal: for number in range(1, self._module_number): name = eval(f"self.module_{number}.name") - module = eval(f'self.module_{number}') print(f'generating image(s) for {name}...', end="") - try: - black, colour = module.generate_image() - if self.show_border: - draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5) - black.save(f"{self.image_folder}module{number}_black.png", "PNG") - colour.save(f"{self.image_folder}module{number}_colour.png", "PNG") + success = self.process_module(number) + if success: print("OK!") - except Exception: + else: errors.append(number) self.info += f"module {number}: Error! " - logger.exception("Error!") - logger.exception(f"Exception: {traceback.format_exc()}.") if errors: logger.error('Error/s in modules:', *errors) @@ -277,14 +242,17 @@ class Inkycal: print("Refresh needed: {a}".format(a=res)) return res - async def run(self): - """Runs main program in nonstop mode. + async def run(self, run_once=False): + """Runs main program in nonstop mode or a single iteration based on the run_once flag. - Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image - from all modules, assembles them in one image, refreshed the E-Paper and - then sleeps until the next scheduled update. + Args: + run_once (bool): If True, runs the updating process once and stops. If False, + runs indefinitely. + + Uses an infinity loop to run Inkycal nonstop or a single time based on run_once. + Inkycal generates the image from all modules, assembles them in one image, + refreshes the E-Paper and then sleeps until the next scheduled update or exits. """ - # Get the time of initial run runtime = arrow.now() @@ -303,31 +271,19 @@ class Inkycal: f"Time: {current_time.format('HH:mm')}") print('Generating images for all modules...', end='') - errors = [] # store module numbers in here + errors = [] # Store module numbers in here - # short info for info-section + # Short info for info-section if not self.settings.get('image_hash', False): self.info = f"{current_time.format('D MMM @ HH:mm')} " else: self.info = "" for number in range(1, self._module_number): - - # name = eval(f"self.module_{number}.name") - module = eval(f'self.module_{number}') - - try: - black, colour = module.generate_image() - if self.show_border: - draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5) - black.save(f"{self.image_folder}module{number}_black.png", "PNG") - colour.save(f"{self.image_folder}module{number}_colour.png", "PNG") - self.info += f"module {number}: OK " - except Exception as e: + success = self.process_module(number) + if not success: errors.append(number) self.info += f"module {number}: Error! " - logger.exception("Error!") - logger.exception(f"Exception: {traceback.format_exc()}.") if errors: logger.error("Error/s in modules:", *errors) @@ -343,10 +299,9 @@ class Inkycal: # Check if image should be rendered if self.render: display = self.Display - self._calibration_check() if self._calibration_state: - # after calibration, we have to forcefully rewrite the screen + # After calibration, we have to forcefully rewrite the screen self._remove_hashes(self.image_folder) if self.supports_colour: @@ -358,17 +313,15 @@ class Inkycal: im_black = upside_down(im_black) im_colour = upside_down(im_colour) - # 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([ (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) # Part for black-white ePapers elif not self.supports_colour: - im_black = self._merge_bands() # Flip the image by 180° if required @@ -376,13 +329,15 @@ 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) print(f'\nNo errors since {counter} display updates \n' f'program started {runtime.humanize()}') + if run_once: + break # Exit the loop after one full cycle if run_once is True + sleep_time = self.countdown() await asyncio.sleep(sleep_time) @@ -392,7 +347,8 @@ class Inkycal: returns the merged image """ - im1_path, im2_path = image_folder + 'canvas.png', image_folder + 'canvas_colour.png' + im1_path = os.path.join(settings.image_folder, "canvas.png") + im2_path = os.path.join(settings.image_folder, "canvas_colour.png") # If there is an image for black and colour, merge them if os.path.exists(im1_path) and os.path.exists(im2_path): @@ -531,7 +487,7 @@ class Inkycal: im_colour = black_to_colour(im_colour) im_colour.paste(im_black, (0, 0), im_black) - im_colour.save(image_folder + 'full-screen.png', 'PNG') + im_colour.save(os.path.join(settings.IMAGE_FOLDER, 'full-screen.png'), 'PNG') @staticmethod def _optimize_im(image, threshold=220): @@ -574,13 +530,29 @@ class Inkycal: @staticmethod def cleanup(): # clean up old images in image_folder - for _file in glob.glob(f"{image_folder}*.png"): + if len(glob.glob(settings.IMAGE_FOLDER)) <= 1: + return + for _file in glob.glob(settings.IMAGE_FOLDER): try: os.remove(_file) except: logger.error(f"could not remove file: {_file}") pass + def process_module(self, number) -> bool or Exception: + """Process individual module to generate images and handle exceptions.""" + module = eval(f'self.module_{number}') + try: + black, colour = module.generate_image() + if self.show_border: + draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5) + black.save(f"{self.image_folder}module{number}_black.png", "PNG") + colour.save(f"{self.image_folder}module{number}_colour.png", "PNG") + return True + except Exception: + logger.exception(f"Error in module {number}!") + return False + if __name__ == '__main__': print(f'running inkycal main in standalone/debug mode') diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py index 55ce044..e48f17f 100644 --- a/inkycal/modules/inkycal_fullweather.py +++ b/inkycal/modules/inkycal_fullweather.py @@ -23,16 +23,18 @@ from icons.weather_icons.weather_icons import get_weather_icon from inkycal.custom.functions import fonts from inkycal.custom.functions import get_system_tz from inkycal.custom.functions import internet_available -from inkycal.custom.functions import top_level from inkycal.custom.inkycal_exceptions import NetworkNotReachableError from inkycal.custom.openweathermap_wrapper import OpenWeatherMap from inkycal.modules.inky_image import image_to_palette from inkycal.modules.template import inkycal_module +from inkycal.settings import Settings logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -icons_dir = os.path.join(top_level, "icons", "ui-icons") +settings = Settings() + +icons_dir = os.path.join(settings.FONT_PATH, "ui-icons") def outline(image: Image, size: int, color: tuple) -> Image: @@ -139,7 +141,7 @@ class Fullweather(inkycal_module): # Check if all required parameters are present for param in self.requires: - if not param in config: + if param not in config: raise Exception(f"config is missing {param}") # required parameters diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index c7481fa..9f098df 100755 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -8,13 +8,13 @@ from inkycal.custom import * # PIL has a class named Image, use alias for Inkyimage -> Images from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette from inkycal.modules.template import inkycal_module +from inkycal.utils.json_cache import JSONCache logger = logging.getLogger(__name__) class Slideshow(inkycal_module): - """Cycles through images in a local image folder - """ + """Cycles through images in a local image folder""" name = "Slideshow - cycle through images from a local folder" requires = { @@ -53,7 +53,7 @@ class Slideshow(inkycal_module): # required parameters for param in self.requires: - if not param in config: + if param not in config: raise Exception(f'config is missing {param}') # optional parameters @@ -64,14 +64,15 @@ class Slideshow(inkycal_module): # Get the full path of all png/jpg/jpeg images in the given folder all_files = glob.glob(f'{self.path}/*') - self.images = [i for i in all_files - if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')] + self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')] if not self.images: - logger.error('No images found in the given folder, please ' - 'double check your path!') + logger.error('No images found in the given folder, please double check your path!') raise Exception('No images found in the given folder path :/') + self.cache = JSONCache('inkycal_slideshow') + self.cache_data = self.cache.read() + # set a 'first run' signal self._first_run = True @@ -89,14 +90,16 @@ class Slideshow(inkycal_module): logger.info(f'Image size: {im_size}') # rotates list items by 1 index - def rotate(somelist): - return somelist[1:] + somelist[:1] + def rotate(list: list): + return list[1:] + list[:1] # Switch to the next image if this is not the first run if self._first_run: self._first_run = False + self.cache_data["current_index"] = 0 else: self.images = rotate(self.images) + self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images) # initialize custom image class im = Images() @@ -110,7 +113,7 @@ class Slideshow(inkycal_module): # Remove background if present im.remove_alpha() - # if autoflip was enabled, flip the image + # if auto-flip was enabled, flip the image if self.autoflip: im.autoflip(self.orientation) @@ -123,6 +126,8 @@ class Slideshow(inkycal_module): # with the images now send, clear the current image im.clear() + self.cache.write(self.cache_data) + # return images return im_black, im_colour diff --git a/inkycal/modules/inkycal_xkcd.py b/inkycal/modules/inkycal_xkcd.py index b2ce25e..63f7140 100644 --- a/inkycal/modules/inkycal_xkcd.py +++ b/inkycal/modules/inkycal_xkcd.py @@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module logger = logging.getLogger(__name__) +settings = Settings() + class Xkcd(inkycal_module): name = "xkcd - Displays comics from xkcd.com by Randall Munroe" @@ -57,7 +59,7 @@ class Xkcd(inkycal_module): """Generate image for this module""" # Create tmp path - tmpPath = f"{top_level}/temp" + tmpPath = settings.TEMPORARY_FOLDER if not os.path.exists(tmpPath): os.mkdir(tmpPath) diff --git a/inkycal/settings.py b/inkycal/settings.py new file mode 100644 index 0000000..8f40352 --- /dev/null +++ b/inkycal/settings.py @@ -0,0 +1,20 @@ +"""Settings class +Used to initialize the settings for the application. +""" +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + + +class Settings: + """Settings class to initialize the settings for the application. + + """ + CACHE_PATH = os.path.join(basedir, "cache") + LOG_PATH = os.path.join(basedir, "logs") + INKYCAL_LOG_PATH = os.path.join(LOG_PATH, "inkycal.log") + FONT_PATH = os.path.join(basedir, "../fonts") + IMAGE_FOLDER = os.path.join(basedir, "../image_folder") + PARALLEL_DRIVER_PATH = os.path.join(basedir, "inkycal", "display", "drivers", "parallel_drivers") + TEMPORARY_FOLDER = os.path.join(basedir, "tmp") + VCOM = "2.0" diff --git a/inkycal/utils/json_cache.py b/inkycal/utils/json_cache.py new file mode 100644 index 0000000..76e8e19 --- /dev/null +++ b/inkycal/utils/json_cache.py @@ -0,0 +1,28 @@ +"""JSON Cache +Can be used to cache JSON data to disk. This is useful for caching data to survive reboots. +""" +import json +import os + +from inkycal.settings import Settings + +settings = Settings() + + +class JSONCache: + def __init__(self, name: str, create_if_not_exists: bool = True): + self.path = os.path.join(settings.CACHE_PATH,f"{name}.json") + if create_if_not_exists and not os.path.exists(self.path): + with open(self.path, "w", encoding="utf-8") as file: + json.dump({}, file) + + def read(self): + try: + with open(self.path, "r", encoding="utf-8") as file: + return json.load(file) + except FileNotFoundError: + return {} + + def write(self, data: dict): + with open(self.path, "w", encoding="utf-8") as file: + json.dump(data, file, indent=4, sort_keys=True)