From 9346fcf750ad7612f4f045fe2b4c1f6e639afaef Mon Sep 17 00:00:00 2001 From: Ace Date: Sat, 11 May 2024 23:25:40 +0200 Subject: [PATCH 01/62] code quality improvements --- inkycal/__init__.py | 2 +- inkycal/display/display.py | 6 +----- inkycal/display/drivers/epdconfig.py | 3 --- inkycal/modules/inkycal_calendar.py | 6 +++--- tests/test_inkycal_agenda.py | 2 +- tests/test_inkycal_webshot.py | 10 ++++++++-- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/inkycal/__init__.py b/inkycal/__init__.py index 0c6a244..6c721f5 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -14,7 +14,7 @@ import inkycal.modules.inkycal_stocks import inkycal.modules.inkycal_webshot import inkycal.modules.inkycal_xkcd import inkycal.modules.inkycal_fullweather +import inkycal.modules.inkycal_mawaqit # Main file from inkycal.main import Inkycal -import inkycal.modules.inkycal_stocks diff --git a/inkycal/display/display.py b/inkycal/display/display.py index 89fdf4c..179f71f 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -2,13 +2,11 @@ Inkycal ePaper driving functions Copyright by aceisace """ -import os 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 @@ -199,9 +197,7 @@ class Display: >>> Display.get_display_names() """ - driver_files = top_level + '/inkycal/display/drivers/' - drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i] - return drivers + return supported_models.keys() if __name__ == '__main__': diff --git a/inkycal/display/drivers/epdconfig.py b/inkycal/display/drivers/epdconfig.py index 2eaddf4..2a5e819 100644 --- a/inkycal/display/drivers/epdconfig.py +++ b/inkycal/display/drivers/epdconfig.py @@ -28,8 +28,6 @@ THE SOFTWARE. """ import logging -import os -import subprocess import sys import time @@ -128,4 +126,3 @@ implementation = RaspberryPi() for func in [x for x in dir(implementation) if not x.startswith('_')]: setattr(sys.modules[__name__], func, getattr(implementation, func)) - diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index 9c85984..8d0cd50 100755 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -109,7 +109,7 @@ class Calendar(inkycal_module): # Allocate space for month-names, weekdays etc. month_name_height = int(im_height * 0.10) text_bbox_height = self.font.getbbox("hg") - weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25) + weekdays_height = int((text_bbox_height[3] - text_bbox_height[1]) * 1.25) logger.debug(f"month_name_height: {month_name_height}") logger.debug(f"weekdays_height: {weekdays_height}") @@ -265,7 +265,7 @@ class Calendar(inkycal_module): # find out how many lines can fit at max in the event section line_spacing = 2 text_bbox_height = self.font.getbbox("hg") - line_height = text_bbox_height[3] + line_spacing + line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing max_event_lines = events_height // (line_height + line_spacing) # generate list of coordinates for each line @@ -326,7 +326,7 @@ class Calendar(inkycal_module): (icon_width, icon_height), radius=6, thickness=1, - shrinkage=(0.4, 0.2), + shrinkage=(0, 0), ) # Filter upcoming events until 4 weeks in the future diff --git a/tests/test_inkycal_agenda.py b/tests/test_inkycal_agenda.py index af002ec..642f654 100755 --- a/tests/test_inkycal_agenda.py +++ b/tests/test_inkycal_agenda.py @@ -28,7 +28,7 @@ tests = [ "padding_x": 10, "padding_y": 10, "fontsize": 12, - "language": "en" + "language": "de" } }, { diff --git a/tests/test_inkycal_webshot.py b/tests/test_inkycal_webshot.py index 7105a12..b86c965 100755 --- a/tests/test_inkycal_webshot.py +++ b/tests/test_inkycal_webshot.py @@ -6,10 +6,15 @@ import logging import unittest from inkycal.modules import Webshot +from inkycal.modules.inky_image import Inkyimage +from tests import Config logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) +preview = Inkyimage.preview +merge = Inkyimage.merge + tests = [ { "position": 1, @@ -60,6 +65,7 @@ class TestWebshot(unittest.TestCase): for test in tests: logger.info(f'test {tests.index(test) + 1} generating image..') module = Webshot(test) - module.generate_image() + im_black, im_colour = module.generate_image() + if Config.USE_PREVIEW: + preview(merge(im_black, im_colour)) logger.info('OK') - From 610f246c0276e4145218133d7b02f169213a1709 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 12 May 2024 02:00:26 +0200 Subject: [PATCH 02/62] checkpoint --- {icons => fonts}/ui-icons/home_temp.png | Bin {icons => fonts}/ui-icons/humidity.bmp | Bin .../outline_thermostat_white_48dp.bmp | Bin {icons => fonts}/ui-icons/rain-chance.bmp | Bin {icons => fonts}/ui-icons/uv.bmp | Bin {icons => fonts}/ui-icons/wind.bmp | Bin inkycal/__init__.py | 13 +- inkycal/custom/functions.py | 26 +-- inkycal/display/drivers/10_in_3.py | 14 +- inkycal/display/drivers/7_in_8.py | 14 +- inkycal/display/drivers/9_in_7.py | 12 +- inkycal/loggers.py | 35 +++ inkycal/main.py | 206 ++++++++---------- inkycal/modules/inkycal_fullweather.py | 8 +- inkycal/modules/inkycal_slideshow.py | 25 ++- inkycal/modules/inkycal_xkcd.py | 4 +- inkycal/settings.py | 20 ++ inkycal/utils/json_cache.py | 28 +++ 18 files changed, 225 insertions(+), 180 deletions(-) rename {icons => fonts}/ui-icons/home_temp.png (100%) rename {icons => fonts}/ui-icons/humidity.bmp (100%) rename {icons => fonts}/ui-icons/outline_thermostat_white_48dp.bmp (100%) rename {icons => fonts}/ui-icons/rain-chance.bmp (100%) rename {icons => fonts}/ui-icons/uv.bmp (100%) rename {icons => fonts}/ui-icons/wind.bmp (100%) create mode 100644 inkycal/loggers.py create mode 100644 inkycal/settings.py create mode 100644 inkycal/utils/json_cache.py 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) From e7036093995906586289eb92dc8ac8f1d53f0821 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:48:05 +0000 Subject: [PATCH 03/62] Bump urllib3 from 2.2.0 to 2.2.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.0 to 2.2.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.0...2.2.2) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37d5b99..24d970a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,7 +46,7 @@ types-python-dateutil==2.8.19.20240106 typing_extensions==4.9.0 tzdata==2024.1 tzlocal==5.2 -urllib3==2.2.0 +urllib3==2.2.2 virtualenv==20.25.0 webencodings==0.5.1 x-wr-timezone==0.0.6 From bf9f7db5acdadfcb25ac269bc04686828d0c98ac Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 20 Jun 2024 22:31:17 +0200 Subject: [PATCH 04/62] implement rotation for webshot module --- inkycal/modules/inkycal_webshot.py | 31 +++++++++++++++---- tests/test_inkycal_webshot.py | 49 ++++++++++++++++-------------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/inkycal/modules/inkycal_webshot.py b/inkycal/modules/inkycal_webshot.py index 8d08cc6..50b7fb0 100644 --- a/inkycal/modules/inkycal_webshot.py +++ b/inkycal/modules/inkycal_webshot.py @@ -41,7 +41,10 @@ class Webshot(inkycal_module): }, "crop_h": { "label": "Please enter the crop height", - } + }, + "rotation": { + "label": "Please enter the rotation. Must be either 0, 90, 180 or 270", + }, } def __init__(self, config): @@ -73,6 +76,12 @@ class Webshot(inkycal_module): else: self.crop_y = 0 + self.rotation = 0 + if "rotation" in config: + self.rotation = int(config["rotation"]) + if self.rotation not in [0, 90, 180, 270]: + raise Exception("Rotation must be either 0, 90, 180 or 270") + # give an OK message print(f'Inkycal webshot loaded') @@ -105,7 +114,7 @@ class Webshot(inkycal_module): logger.info( f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}') - shot = WebShot() + shot = WebShot(size=(im_height, im_width)) shot.params = { "--crop-x": self.crop_x, @@ -151,11 +160,21 @@ class Webshot(inkycal_module): centerPosX = int((im_width / 2) - (im.image.width / 2)) - webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY)) - im_black.paste(webshotSpaceBlack) - webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY)) - im_colour.paste(webshotSpaceColour) + if self.rotation != 0: + webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY)) + im_black.paste(webshotSpaceBlack) + im_black = im_black.rotate(self.rotation, expand=True) + + webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY)) + im_colour.paste(webshotSpaceColour) + im_colour = im_colour.rotate(self.rotation, expand=True) + else: + webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY)) + im_black.paste(webshotSpaceBlack) + + webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY)) + im_colour.paste(webshotSpaceColour) im.clear() logger.info(f'added webshot image') diff --git a/tests/test_inkycal_webshot.py b/tests/test_inkycal_webshot.py index 7105a12..d8e1b12 100755 --- a/tests/test_inkycal_webshot.py +++ b/tests/test_inkycal_webshot.py @@ -11,33 +11,13 @@ logger = logging.getLogger(__name__) logging.basicConfig(level=logging.DEBUG) tests = [ - { - "position": 1, - "name": "Webshot", - "config": { - "size": [400, 100], - "url": "https://github.com", - "palette": "bwr", - "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" - } - }, { "position": 1, "name": "Webshot", "config": { "size": [400, 200], - "url": "https://github.com", - "palette": "bwy", - "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" - } - }, - { - "position": 1, - "name": "Webshot", - "config": { - "size": [400, 300], - "url": "https://github.com", - "palette": "bw", + "url": "https://aceinnolab.com", + "palette": "bwr", "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } }, @@ -46,8 +26,31 @@ tests = [ "name": "Webshot", "config": { "size": [400, 400], - "url": "https://github.com", + "url": "https://aceinnolab.com", + "palette": "bwy", + "rotation": 0, + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } + }, + { + "position": 1, + "name": "Webshot", + "config": { + "size": [400, 600], + "url": "https://aceinnolab.com", + "palette": "bw", + "rotation": 90, + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } + }, + { + "position": 1, + "name": "Webshot", + "config": { + "size": [400, 800], + "url": "https://aceinnolab.com", "palette": "bwr", + "rotation": 180, "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } } From acc4fb7ce64a56ba637b87f7ed93d663cc77f176 Mon Sep 17 00:00:00 2001 From: Ace Date: Sat, 22 Jun 2024 02:03:48 +0200 Subject: [PATCH 05/62] fix an issue where the text would not be vertically centered --- inkycal/custom/functions.py | 51 ++++++++++++++++------------- inkycal/modules/inkycal_calendar.py | 49 +++++++++++++-------------- tests/test_functions.py | 22 +++++++++---- tests/test_inkycal_calendar.py | 2 +- 4 files changed, 68 insertions(+), 56 deletions(-) diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index 327095c..0bdb419 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -8,17 +8,17 @@ import logging import os import time import traceback +from typing import Tuple import arrow -import PIL import requests import tzlocal from PIL import Image from PIL import ImageDraw from PIL import ImageFont -logs = logging.getLogger(__name__) -logs.setLevel(level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(level=logging.INFO) # Get the path to the Inkycal folder top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1]) @@ -39,7 +39,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()] @@ -77,16 +77,16 @@ def get_system_tz() -> str: >>> import arrow >>> print(arrow.now()) # returns non-timezone-aware time - >>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time. + >>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time. """ 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 @@ -115,7 +115,7 @@ def auto_fontsize(font, max_height): return font -def write(image, xy, box_size, text, font=None, **kwargs): +def write(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], text: str, font=None, **kwargs): """Writes text on an image. Writes given text at given position on the specified image. @@ -165,7 +165,7 @@ def write(image, xy, box_size, text, font=None, **kwargs): 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] + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) while text_width < int(box_width * fill_width) and text_height < int(box_height * fill_height): size += 1 @@ -173,23 +173,23 @@ def write(image, xy, box_size, text, font=None, **kwargs): 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] + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[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] + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) # 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) + text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1]) + logger.debug(text) # Align text to desired position if alignment == "center" or None: @@ -199,10 +199,13 @@ def write(image, xy, box_size, text, font=None, **kwargs): elif alignment == "right": x = int(box_width - text_width) + # Vertical centering + y = int((box_height / 2) - (text_height / 2)) + # Draw the text in the text-box draw = ImageDraw.Draw(image) space = Image.new('RGBA', (box_width, box_height)) - ImageDraw.Draw(space).text((x, 0), text, fill=colour, font=font) + ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font) # Uncomment following two lines, comment out above two lines to show # red text-box with white text (debugging purposes) @@ -217,7 +220,7 @@ def write(image, xy, box_size, text, font=None, **kwargs): image.paste(space, xy, space) -def text_wrap(text, font=None, max_width=None): +def text_wrap(text: str, font=None, max_width=None): """Splits a very long text into smaller parts Splits a long text to smaller lines which can fit in a line with max_width. @@ -253,7 +256,7 @@ def text_wrap(text, font=None, max_width=None): return lines -def internet_available(): +def internet_available() -> bool: """checks if the internet is available. Attempts to connect to google.com with a timeout of 5 seconds to check @@ -278,15 +281,16 @@ def internet_available(): return False -def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)): +def draw_border(image: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int = 5, thickness: int = 1, + shrinkage: Tuple[int, int] = (0.1, 0.1)) -> None: """Draws a border at given coordinates. Args: - image: The image on which the border should be drawn (usually im_black or - im_colour. + im_colour). - xy: Tuple representing the top-left corner of the border e.g. (32, 100) - where 32 is the x co-ordinate and 100 is the y-coordinate. + where 32 is the x-coordinate and 100 is the y-coordinate. - size: Size of the border as a tuple -> (width, height). @@ -324,6 +328,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)): c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height) c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height) + # Draw lines and arcs, creating a square with round corners draw = ImageDraw.Draw(image) draw.line((p1, p2), fill=colour, width=thickness) @@ -338,7 +343,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)): draw.arc((c7, c8), 90, 180, fill=colour, width=thickness) -def draw_border_2(im: PIL.Image, xy: tuple, size: tuple, radius: int): +def draw_border_2(im: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int): draw = ImageDraw.Draw(im) x, y = xy diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index 9c85984..c17582f 100755 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -6,16 +6,16 @@ Copyright by aceinnolab # pylint: disable=logging-fstring-interpolation import calendar as cal -import arrow -from inkycal.modules.template import inkycal_module + from inkycal.custom import * +from inkycal.modules.template import inkycal_module logger = logging.getLogger(__name__) class Calendar(inkycal_module): """Calendar class - Create monthly calendar and show events from given icalendars + Create monthly calendar and show events from given iCalendars """ name = "Calendar - Show monthly calendar with events from iCalendars" @@ -39,12 +39,12 @@ class Calendar(inkycal_module): }, "date_format": { "label": "Use an arrow-supported token for custom date formatting " - + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", + + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", "default": "D MMM", }, "time_format": { "label": "Use an arrow-supported token for custom time formatting " - + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", + + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", "default": "HH:mm", }, } @@ -61,7 +61,7 @@ class Calendar(inkycal_module): self._days_with_events = None # optional parameters - self.weekstart = config['week_starts_on'] + self.week_start = config['week_starts_on'] self.show_events = config['show_events'] self.date_format = config["date_format"] self.time_format = config['time_format'] @@ -109,7 +109,7 @@ class Calendar(inkycal_module): # Allocate space for month-names, weekdays etc. month_name_height = int(im_height * 0.10) text_bbox_height = self.font.getbbox("hg") - weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25) + weekdays_height = int((abs(text_bbox_height[3]) + abs(text_bbox_height[1])) * 1.25) logger.debug(f"month_name_height: {month_name_height}") logger.debug(f"weekdays_height: {weekdays_height}") @@ -117,7 +117,7 @@ class Calendar(inkycal_module): logger.debug("Allocating space for events") calendar_height = int(im_height * 0.6) events_height = ( - im_height - month_name_height - weekdays_height - calendar_height + im_height - month_name_height - weekdays_height - calendar_height ) logger.debug(f'calendar-section size: {im_width} x {calendar_height} px') logger.debug(f'events-section size: {im_width} x {events_height} px') @@ -156,13 +156,13 @@ class Calendar(inkycal_module): now = arrow.now(tz=self.timezone) - # Set weekstart of calendar to specified weekstart - if self.weekstart == "Monday": + # Set week-start of calendar to specified week-start + if self.week_start == "Monday": cal.setfirstweekday(cal.MONDAY) - weekstart = now.shift(days=-now.weekday()) + week_start = now.shift(days=-now.weekday()) else: cal.setfirstweekday(cal.SUNDAY) - weekstart = now.shift(days=-now.isoweekday()) + week_start = now.shift(days=-now.isoweekday()) # Write the name of current month write( @@ -174,9 +174,9 @@ class Calendar(inkycal_module): autofit=True, ) - # Set up weeknames in local language and add to main section + # Set up week-names in local language and add to main section weekday_names = [ - weekstart.shift(days=+_).format('ddd', locale=self.language) + week_start.shift(days=+_).format('ddd', locale=self.language) for _ in range(7) ] logger.debug(f'weekday names: {weekday_names}') @@ -192,7 +192,7 @@ class Calendar(inkycal_module): fill_height=0.9, ) - # Create a calendar template and flatten (remove nestings) + # Create a calendar template and flatten (remove nesting) calendar_flat = self.flatten(cal.monthcalendar(now.year, now.month)) # logger.debug(f" calendar_flat: {calendar_flat}") @@ -281,7 +281,7 @@ class Calendar(inkycal_module): month_start = arrow.get(now.floor('month')) month_end = arrow.get(now.ceil('month')) - # fetch events from given icalendars + # fetch events from given iCalendars self.ical = iCalendar() parser = self.ical @@ -294,14 +294,12 @@ class Calendar(inkycal_module): month_events = parser.get_events(month_start, month_end, self.timezone) parser.sort() self.month_events = month_events - + # Initialize days_with_events as an empty list days_with_events = [] # Handle multi-day events by adding all days between start and end for event in month_events: - start_date = event['begin'].date() - end_date = event['end'].date() # Convert start and end dates to arrow objects with timezone start = arrow.get(event['begin'].date(), tzinfo=self.timezone) @@ -325,8 +323,6 @@ class Calendar(inkycal_module): grid[days], (icon_width, icon_height), radius=6, - thickness=1, - shrinkage=(0.4, 0.2), ) # Filter upcoming events until 4 weeks in the future @@ -345,13 +341,13 @@ class Calendar(inkycal_module): date_width = int(max(( self.font.getlength(events['begin'].format(self.date_format, locale=lang)) - for events in upcoming_events))* 1.1 - ) + for events in upcoming_events)) * 1.1 + ) time_width = int(max(( self.font.getlength(events['begin'].format(self.time_format, locale=lang)) - for events in upcoming_events))* 1.1 - ) + for events in upcoming_events)) * 1.1 + ) text_bbox_height = self.font.getbbox("hg") line_height = text_bbox_height[3] + line_spacing @@ -369,7 +365,8 @@ class Calendar(inkycal_module): event_duration = (event['end'] - event['begin']).days if event_duration > 1: # Format the duration using Arrow's localization - days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang) + days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, + locale=lang) the_name = f"{event['title']} ({days_translation})" else: the_name = event['title'] diff --git a/tests/test_functions.py b/tests/test_functions.py index b624978..0d8a6dd 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -1,12 +1,22 @@ """ Test the functions in the functions module. """ +import unittest + from PIL import Image, ImageFont -from inkycal.custom import write, fonts + +from inkycal.custom import write, fonts, get_system_tz -def test_write(): - im = Image.new("RGB", (500, 200), "white") - font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size = 40) - write(im, (125,75), (250, 50), "Hello World", font) - # im.show() +class TestIcalendar(unittest.TestCase): + + def test_write(self): + im = Image.new("RGB", (500, 200), "white") + font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size=40) + write(im, (125, 75), (250, 50), "Hello World", font) + # im.show() + + def test_get_system_tz(self): + tz = get_system_tz() + assert isinstance(tz, str) + diff --git a/tests/test_inkycal_calendar.py b/tests/test_inkycal_calendar.py index cb28b9a..434d5d8 100755 --- a/tests/test_inkycal_calendar.py +++ b/tests/test_inkycal_calendar.py @@ -20,7 +20,7 @@ tests = [ { "name": "Calendar", "config": { - "size": [500, 500], + "size": [500, 600], "week_starts_on": "Monday", "show_events": True, "ical_urls": sample_url, From 544f6063798f15b5ceceeb2bc8b4f8068a271fbb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 22 Jun 2024 00:47:19 +0000 Subject: [PATCH 06/62] update docs [bot] --- docs/inkycal.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/inkycal.html b/docs/inkycal.html index 2166d2b..e098b4e 100644 --- a/docs/inkycal.html +++ b/docs/inkycal.html @@ -232,14 +232,14 @@ which the given font should be scaled to.

-inkycal.custom.functions.draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1))
+inkycal.custom.functions.draw_border(image: <module 'PIL.Image' from '/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>, xy: ~typing.Tuple[int, int], size: ~typing.Tuple[int, int], radius: int = 5, thickness: int = 1, shrinkage: ~typing.Tuple[int, int] = (0.1, 0.1)) None

Draws a border at given coordinates.

Args:
  • image: The image on which the border should be drawn (usually im_black or -im_colour.

  • +im_colour).

  • xy: Tuple representing the top-left corner of the border e.g. (32, 100) -where 32 is the x co-ordinate and 100 is the y-coordinate.

  • +where 32 is the x-coordinate and 100 is the y-coordinate.

  • size: Size of the border as a tuple -> (width, height).

  • radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.

  • thickness: Thickness of the border in pixels.

  • @@ -288,14 +288,14 @@ printed fonts of this function:

    The extracted timezone can be used to show the local time instead of UTC. e.g.

    >>> import arrow
     >>> print(arrow.now()) # returns non-timezone-aware time
    ->>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
    +>>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time.
     
-inkycal.custom.functions.internet_available()
+inkycal.custom.functions.internet_available() bool

checks if the internet is available.

Attempts to connect to google.com with a timeout of 5 seconds to check if the network can be reached.

@@ -315,7 +315,7 @@ if the network can be reached.

-inkycal.custom.functions.text_wrap(text, font=None, max_width=None)
+inkycal.custom.functions.text_wrap(text: str, font=None, max_width=None)

Splits a very long text into smaller parts

Splits a long text to smaller lines which can fit in a line with max_width. Uses a Font object for more accurate calculations.

@@ -334,7 +334,7 @@ splitting the text into the next chunk.

-inkycal.custom.functions.write(image, xy, box_size, text, font=None, **kwargs)
+inkycal.custom.functions.write(image: <module 'PIL.Image' from '/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>, xy: ~typing.Tuple[int, int], box_size: ~typing.Tuple[int, int], text: str, font=None, **kwargs)

Writes text on an image.

Writes given text at given position on the specified image.

From 4f7d31f54e822e915323fbef2d29882bd98a82e5 Mon Sep 17 00:00:00 2001 From: Ace Date: Sat, 22 Jun 2024 02:56:45 +0200 Subject: [PATCH 07/62] fix missing translation add material-icons font use icon for all-day events --- fonts/MaterialIcons/MaterialIcons.ttf | Bin 0 -> 356840 bytes inkycal/custom/openweathermap_wrapper.py | 27 ++---- inkycal/modules/inkycal_agenda.py | 8 +- inkycal/modules/inkycal_weather.py | 113 ++++++++++++----------- tests/test_inkycal_agenda.py | 2 +- tests/test_inkycal_weather.py | 2 +- 6 files changed, 75 insertions(+), 77 deletions(-) create mode 100644 fonts/MaterialIcons/MaterialIcons.ttf diff --git a/fonts/MaterialIcons/MaterialIcons.ttf b/fonts/MaterialIcons/MaterialIcons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9d09b0feb85c35beeaddd31246be0b7c8e0e69a4 GIT binary patch literal 356840 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

t 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 literal 0 HcmV?d00001 diff --git a/inkycal/custom/openweathermap_wrapper.py b/inkycal/custom/openweathermap_wrapper.py index 6cd4405..779c5bf 100644 --- a/inkycal/custom/openweathermap_wrapper.py +++ b/inkycal/custom/openweathermap_wrapper.py @@ -41,18 +41,9 @@ def get_json_from_url(request_url): class OpenWeatherMap: - def __init__( - self, - api_key: str, - city_id: int = None, - lat: float = None, - lon: float = None, - api_version: API_VERSIONS = "2.5", - temp_unit: TEMP_UNITS = "celsius", - wind_unit: WIND_UNITS = "meters_sec", - language: str = "en", - tz_name: str = "UTC", - ) -> None: + def __init__(self, api_key: str, city_id: int = None, lat: float = None, lon: float = None, + api_version: API_VERSIONS = "2.5", temp_unit: TEMP_UNITS = "celsius", + wind_unit: WIND_UNITS = "meters_sec", language: str = "en", tz_name: str = "UTC") -> None: self.api_key = api_key self.temp_unit = temp_unit self.wind_unit = wind_unit @@ -106,7 +97,7 @@ class OpenWeatherMap: current_weather["temp_feels_like"] = self.get_converted_temperature(current_data["main"]["feels_like"]) current_weather["min_temp"] = self.get_converted_temperature(current_data["main"]["temp_min"]) current_weather["max_temp"] = self.get_converted_temperature(current_data["main"]["temp_max"]) - current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH + current_weather["humidity"] = current_data["main"]["humidity"] # OWM Unit: % rH current_weather["wind"] = self.get_converted_windspeed( current_data["wind"]["speed"] ) # OWM Unit Default: meter/sec, Metric: meter/sec @@ -161,10 +152,10 @@ class OpenWeatherMap: forecast["wind"]["speed"] ), # OWM Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour "wind_gust": self.get_converted_windspeed(forecast["wind"]["gust"]), - "pressure": forecast["main"]["pressure"], # OWM Unit: hPa - "humidity": forecast["main"]["humidity"], # OWM Unit: % rH + "pressure": forecast["main"]["pressure"], # OWM Unit: hPa + "humidity": forecast["main"]["humidity"], # OWM Unit: % rH "precip_probability": forecast["pop"] - * 100.0, # OWM value is unitless, directly converting to % scale + * 100.0, # OWM value is unitless, directly converting to % scale "icon": forecast["weather"][0]["icon"], "datetime": datetime.fromtimestamp(forecast["dt"], tz=self.tz_zone), } @@ -187,7 +178,7 @@ class OpenWeatherMap: :return: Forecast dictionary """ - # Make sure hourly forecasts are up to date + # Make sure hourly forecasts are up-to-date _ = self.get_weather_forecast() # Calculate the start and end times for the specified number of days from now @@ -207,7 +198,7 @@ class OpenWeatherMap: ] # In case the next available forecast is already for the next day, use that one for the less than 3 remaining hours of today - if forecasts == []: + if not forecasts: forecasts.append(self.hourly_forecasts[0]) # Get rain and temperatures for that day diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 5b7f5d0..f695440 100755 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -77,6 +77,8 @@ class Agenda(inkycal_module): # Additional config self.timezone = get_system_tz() + self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize) + # give an OK message print(f'{__name__} loaded') @@ -203,10 +205,10 @@ class Agenda(inkycal_module): write(im_black, (x_time, line_pos[cursor][1]), (time_width, line_height), time, font=self.font, alignment='right') - if parser.all_day(_): + else: write(im_black, (x_time, line_pos[cursor][1]), - (time_width, line_height), "all day", - font=self.font, alignment='right') + (time_width, line_height), "\ue878", + font=self.icon_font, alignment='right') write(im_black, (x_event, line_pos[cursor][1]), (event_width, line_height), diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 7aeb845..6bd61c1 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -2,12 +2,12 @@ Inkycal weather module Copyright by aceinnolab """ - -import arrow import decimal import logging import math +from typing import Tuple +import arrow from PIL import Image from PIL import ImageDraw from PIL import ImageFont @@ -51,7 +51,7 @@ class Weather(inkycal_module): "options": [True, False], }, - "round_windspeed": { + "round_wind_speed": { "label": "Round windspeed?", "options": [True, False], }, @@ -89,7 +89,7 @@ class Weather(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 @@ -98,15 +98,15 @@ class Weather(inkycal_module): # optional parameters self.round_temperature = config['round_temperature'] - self.round_windspeed = config['round_windspeed'] + self.round_wind_speed = config['round_windspeed'] self.forecast_interval = config['forecast_interval'] self.hour_format = int(config['hour_format']) if config['units'] == "imperial": self.temp_unit = "fahrenheit" else: self.temp_unit = "celsius" - - if config['use_beaufort'] == True: + + if config['use_beaufort']: self.wind_unit = "beaufort" elif config['units'] == "imperial": self.wind_unit = "miles_hour" @@ -116,17 +116,17 @@ class Weather(inkycal_module): # additional configuration self.owm = OpenWeatherMap( - api_key=self.api_key, - city_id=self.location, - wind_unit=self.wind_unit, + api_key=self.api_key, + city_id=self.location, + wind_unit=self.wind_unit, temp_unit=self.temp_unit, - language=self.locale, + language=self.locale, tz_name=self.timezone - ) - + ) + self.weatherfont = ImageFont.truetype( fonts['weathericons-regular-webfont'], size=self.fontsize) - + if self.wind_unit == "beaufort": self.windDispUnit = "bft" elif self.wind_unit == "knots": @@ -145,8 +145,6 @@ class Weather(inkycal_module): # give an OK message print(f"{__name__} loaded") - - def generate_image(self): """Generate image for this module""" @@ -190,7 +188,7 @@ class Weather(inkycal_module): 7: '\uf0ae' }[int(index) & 7] - def is_negative(temp:str): + def is_negative(temp: str): """Check if temp is below freezing point of water (0°C/32°F) returns True if temp below freezing point, else False""" answer = False @@ -223,12 +221,19 @@ class Weather(inkycal_module): '50n': '\uf023' } - def draw_icon(image, xy, box_size, icon, rotation=None): - """Custom function to add icons of weather font on image - image = on which image should the text be added? - xy = xy-coordinates as tuple -> (x,y) - box_size = size of text-box -> (width,height) - icon = icon-unicode, looks this up in weathericons dictionary + def draw_icon(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], icon: str, rotation=None): + """Custom function to add icons of weather font on the image. + + Args: + - image: + the image on which image should the text be added + - xy: + coordinates as tuple -> (x,y) + - box_size: + size of text-box -> (width,height) + - icon: + icon-unicode, looks this up in weather-icons dictionary + """ icon_size_correction = { @@ -263,7 +268,6 @@ class Weather(inkycal_module): '\uf0a0': 0, '\uf0a3': 0, '\uf0a7': 0, - '\uf0aa': 0, '\uf0ae': 0 } @@ -277,8 +281,7 @@ class Weather(inkycal_module): font = ImageFont.truetype(font.path, size) text_width, text_height = font.getbbox(text)[2:] - while (text_width < int(box_width * 0.9) and - text_height < int(box_height * 0.9)): + while text_width < int(box_width * 0.9) and text_height < int(box_height * 0.9): size += 1 font = ImageFont.truetype(font.path, size) text_width, text_height = font.getbbox(text)[2:] @@ -289,8 +292,6 @@ class Weather(inkycal_module): x = int((box_width / 2) - (text_width / 2)) y = int((box_height / 2) - (text_height / 2)) - # Draw the text in the text-box - draw = ImageDraw.Draw(image) space = Image.new('RGBA', (box_width, box_height)) ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) @@ -349,17 +350,17 @@ class Weather(inkycal_module): row3 = row2 + line_gap + row_height # Draw lines on each row and border - ############################################################################ - ## draw = ImageDraw.Draw(im_black) - ## draw.line((0, 0, im_width, 0), fill='red') - ## draw.line((0, im_height-1, im_width, im_height-1), fill='red') - ## draw.line((0, row1, im_width, row1), fill='black') - ## draw.line((0, row1+row_height, im_width, row1+row_height), fill='black') - ## draw.line((0, row2, im_width, row2), fill='black') - ## draw.line((0, row2+row_height, im_width, row2+row_height), fill='black') - ## draw.line((0, row3, im_width, row3), fill='black') - ## draw.line((0, row3+row_height, im_width, row3+row_height), fill='black') - ############################################################################ + ########################################################################### + # draw = ImageDraw.Draw(im_black) + # draw.line((0, 0, im_width, 0), fill='red') + # draw.line((0, im_height-1, im_width, im_height-1), fill='red') + # draw.line((0, row1, im_width, row1), fill='black') + # draw.line((0, row1+row_height, im_width, row1+row_height), fill='black') + # draw.line((0, row2, im_width, row2), fill='black') + # draw.line((0, row2+row_height, im_width, row2+row_height), fill='black') + # draw.line((0, row3, im_width, row3), fill='black') + # draw.line((0, row3+row_height, im_width, row3+row_height), fill='black') + ########################################################################### # Positions for current weather details weather_icon_pos = (col1, 0) @@ -378,24 +379,24 @@ class Weather(inkycal_module): sunset_time_pos = (col3 + icon_small, row3) # Positions for forecast 1 - stamp_fc1 = (col4, row1) - icon_fc1 = (col4, row1 + row_height) - temp_fc1 = (col4, row3) + stamp_fc1 = (col4, row1) # noqa + icon_fc1 = (col4, row1 + row_height) # noqa + temp_fc1 = (col4, row3) # noqa # Positions for forecast 2 - stamp_fc2 = (col5, row1) - icon_fc2 = (col5, row1 + row_height) - temp_fc2 = (col5, row3) + stamp_fc2 = (col5, row1) # noqa + icon_fc2 = (col5, row1 + row_height) # noqa + temp_fc2 = (col5, row3) # noqa # Positions for forecast 3 - stamp_fc3 = (col6, row1) - icon_fc3 = (col6, row1 + row_height) - temp_fc3 = (col6, row3) + stamp_fc3 = (col6, row1) # noqa + icon_fc3 = (col6, row1 + row_height) # noqa + temp_fc3 = (col6, row3) # noqa # Positions for forecast 4 - stamp_fc4 = (col7, row1) - icon_fc4 = (col7, row1 + row_height) - temp_fc4 = (col7, row3) + stamp_fc4 = (col7, row1) # noqa + icon_fc4 = (col7, row1 + row_height) # noqa + temp_fc4 = (col7, row3) # noqa # Create current-weather and weather-forecast objects logging.debug('looking up location by ID') @@ -404,7 +405,7 @@ class Weather(inkycal_module): # Set decimals dec_temp = 0 if self.round_temperature == True else 1 - dec_wind = 0 if self.round_windspeed == True else 1 + dec_wind = 0 if self.round_wind_speed == True else 1 logging.debug(f'temperature unit: {self.temp_unit}') logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}') @@ -424,7 +425,8 @@ class Weather(inkycal_module): fc_data['fc' + str(index + 1)] = { 'temp': f"{forecast['temp']:.{dec_temp}f}{self.tempDispUnit}", 'icon': forecast["icon"], - 'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M")} + 'stamp': forecast["datetime"].strftime("%I %p" if self.hour_format == 12 else "%H:%M") + } elif self.forecast_interval == 'daily': @@ -433,7 +435,7 @@ class Weather(inkycal_module): daily_forecasts = [self.owm.get_forecast_for_day(days) for days in range(1, 5)] for index, forecast in enumerate(daily_forecasts): - fc_data['fc' + str(index +1)] = { + fc_data['fc' + str(index + 1)] = { 'temp': f'{forecast["temp_min"]:.{dec_temp}f}{self.tempDispUnit}/{forecast["temp_max"]:.{dec_temp}f}{self.tempDispUnit}', 'icon': forecast['icon'], 'stamp': forecast['datetime'].strftime("%A") @@ -513,6 +515,9 @@ class Weather(inkycal_module): # Add the forecast data to the correct places for pos in range(1, len(fc_data) + 1): stamp = fc_data[f'fc{pos}']['stamp'] + # check if we're using daily forecasts + if "day" in stamp: + stamp = arrow.get(fc_data[f'fc{pos}']['stamp'], "dddd").format("dddd", locale="de") icon = weather_icons[fc_data[f'fc{pos}']['icon']] temp = fc_data[f'fc{pos}']['temp'] diff --git a/tests/test_inkycal_agenda.py b/tests/test_inkycal_agenda.py index af002ec..cc4903a 100755 --- a/tests/test_inkycal_agenda.py +++ b/tests/test_inkycal_agenda.py @@ -37,7 +37,7 @@ tests = [ "size": [500, 800], "ical_urls": sample_url, "ical_files": None, - "date_format": "ddd D MMM", + "date_format": "DD.MMMM YYYY", "time_format": "HH:mm", "padding_x": 10, "padding_y": 10, diff --git a/tests/test_inkycal_weather.py b/tests/test_inkycal_weather.py index bcc50ce..2325616 100755 --- a/tests/test_inkycal_weather.py +++ b/tests/test_inkycal_weather.py @@ -34,7 +34,7 @@ tests = [ "padding_x": 10, "padding_y": 10, "fontsize": 12, - "language": "en" + "language": "de" } }, { From 309c6ca5bc09776e7bd6d9858ad1969ec151ebe2 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 20:13:58 +0200 Subject: [PATCH 08/62] test pisugar support --- inky_run.py | 7 ++- inkycal/main.py | 2 +- inkycal/modules/inkycal_slideshow.py | 2 +- inkycal/utils/__init__.py | 2 + inkycal/utils/pisugar.py | 78 ++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 inkycal/utils/__init__.py create mode 100644 inkycal/utils/pisugar.py diff --git a/inky_run.py b/inky_run.py index e2fff6a..2fb9718 100644 --- a/inky_run.py +++ b/inky_run.py @@ -1,7 +1,12 @@ +"""Basic Inkycal run script. + +Assumes that the settings.json file is in the /boot directory. +set render=True to render the display, set render=False to only run the modules. +""" import asyncio from inkycal import Inkycal inky = Inkycal(render=True) # Initialise Inkycal # If your settings.json file is not in /boot, use the full path: inky = Inkycal('path/to/settings.json', render=True) -inky.test() # test if Inkycal can be run correctly, running this will show a bit of info for each module +inky.run(run_once=True) # test if Inkycal can be run correctly, running this will show a bit of info for each module asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop diff --git a/inkycal/main.py b/inkycal/main.py index ac3dd88..c1dd0f1 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -13,7 +13,7 @@ from inkycal import loggers # noqa from inkycal.custom import * from inkycal.display import Display from inkycal.modules.inky_image import Inkyimage as Images -from inkycal.utils.json_cache import JSONCache +from inkycal.utils import JSONCache logger = logging.getLogger(__name__) diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index 9f098df..253397d 100755 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -8,7 +8,7 @@ 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 +from inkycal.utils import JSONCache logger = logging.getLogger(__name__) diff --git a/inkycal/utils/__init__.py b/inkycal/utils/__init__.py new file mode 100644 index 0000000..e0a85ab --- /dev/null +++ b/inkycal/utils/__init__.py @@ -0,0 +1,2 @@ +from .pisugar import PiSugar +from .json_cache import JSONCache \ No newline at end of file diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py new file mode 100644 index 0000000..7f00cee --- /dev/null +++ b/inkycal/utils/pisugar.py @@ -0,0 +1,78 @@ +"""PiSugar helper class for Inkycal.""" + +import logging +import subprocess + +from inkycal.settings import Settings + +settings = Settings() + +logger = logging.getLogger(__name__) + + +class PiSugar: + + def __init__(self): + # replace "command" with actual command + self.command_template = 'echo "command" | nc -q 0 127.0.0.1 8423' + self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled", + "get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc"] + + def _get_output(self, command): + if command not in self.allowed_commands: + logger.error(f"Command {command} not allowed") + return None + cmd = self.command_template.replace("command", command) + try: + result = subprocess.run(cmd, text=True, capture_output=True) + if result.returncode != 0: + print(f"Command failed with {result.stderr}") + return None + return result + except Exception as e: + logger.error(f"Error executing command: {e}") + return None + + def get_battery(self) -> int or None: + """Get the battery level in percentage. + + Returns: + int or None: The battery level in percentage or None if the command fails. + """ + battery_output = self._get_output("get battery") + if battery_output: + for line in battery_output.splitlines(): + if 'battery:' in line: + return int(line.split(':')[1].strip()) + return None + + def get_model(self): + """Get the PiSugar model.""" + model_output = self._get_output("get model") + if model_output: + for line in model_output.splitlines(): + if 'model:' in line: + return line.split(':')[1].strip() + return None + + def get_rtc_time(self): + """Get the RTC time.""" + return self._get_output("get rtc_time") + + def get_rtc_alarm_enabled(self): + """Get the RTC alarm enabled status.""" + return self._get_output("get rtc_alarm_enabled") + + def get_rtc_alarm_time(self): + """Get the RTC alarm time.""" + return self._get_output("get rtc_alarm_time") + + def get_alarm_repeat(self): + """Get the alarm repeat status.""" + return self._get_output("get alarm_repeat") + + def rtc_pi2rtc(self): + """Sync the Pi time to RTC.""" + return self._get_output("rtc_pi2rtc") + + From 13e03331f807cdbe2dec5ca1edee99b866a9c2e7 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 21:08:01 +0200 Subject: [PATCH 09/62] fix some minor bugs --- inkycal/utils/pisugar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 7f00cee..5c4bf8c 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -24,11 +24,12 @@ class PiSugar: return None cmd = self.command_template.replace("command", command) try: - result = subprocess.run(cmd, text=True, capture_output=True) + result = subprocess.run(cmd, shell=True, text=True, capture_output=True) if result.returncode != 0: print(f"Command failed with {result.stderr}") return None - return result + output = result.stdout.strip() + return output except Exception as e: logger.error(f"Error executing command: {e}") return None From 0ecca08484c5aa621688f1e0d83e03560ae8876d Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 21:24:59 +0200 Subject: [PATCH 10/62] improve pisugar class --- inkycal/utils/pisugar.py | 73 +++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 5c4bf8c..43fc4e3 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -47,7 +47,7 @@ class PiSugar: return int(line.split(':')[1].strip()) return None - def get_model(self): + def get_model(self) -> str or None: """Get the PiSugar model.""" model_output = self._get_output("get model") if model_output: @@ -56,24 +56,71 @@ class PiSugar: return line.split(':')[1].strip() return None - def get_rtc_time(self): + def get_rtc_time(self) -> str or None: """Get the RTC time.""" - return self._get_output("get rtc_time") + result = self._get_output("get rtc_time") + if result: + second_line = result.splitlines()[1] + return second_line.split('rtc_alarm_time: ')[1].strip() + return None - def get_rtc_alarm_enabled(self): + def get_rtc_alarm_enabled(self) -> str or None: """Get the RTC alarm enabled status.""" - return self._get_output("get rtc_alarm_enabled") + result = self._get_output("get rtc_alarm_enabled") + if result: + second_line = result.splitlines()[1] + return second_line.split('rtc_alarm_enabled: ')[1].strip() + return None - def get_rtc_alarm_time(self): + def get_rtc_alarm_time(self) -> str or None: """Get the RTC alarm time.""" - return self._get_output("get rtc_alarm_time") + result = self._get_output("get rtc_alarm_time") + if result: + second_line = result.splitlines()[1] + return second_line.split('rtc_alarm_time: ')[1].strip() + return None - def get_alarm_repeat(self): - """Get the alarm repeat status.""" - return self._get_output("get alarm_repeat") + def get_alarm_repeat(self) -> dict or None: + """Get the alarm repeat status. - def rtc_pi2rtc(self): - """Sync the Pi time to RTC.""" - return self._get_output("rtc_pi2rtc") + Returns: + dict or None: A dictionary with the alarm repeating days or None if the command fails. + """ + result = self._get_output("get alarm_repeat") + if result: + second_line = result.splitlines()[1] + repeating_days = second_line.split('alarm_repeat: ')[1].strip() + data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False, + "Saturday": False, "Sunday": False} + if repeating_days[0] == "1": + data["Monday"] = True + if repeating_days[1] == "1": + data["Tuesday"] = True + if repeating_days[2] == "1": + data["Wednesday"] = True + if repeating_days[3] == "1": + data["Thursday"] = True + if repeating_days[4] == "1": + data["Friday"] = True + if repeating_days[5] == "1": + data["Saturday"] = True + if repeating_days[6] == "1": + data["Sunday"] = True + return data + return None + + def rtc_pi2rtc(self) -> bool: + """Sync the Pi time to RTC. + + Returns: + bool: True if the sync was successful, False otherwise. + """ + result = self._get_output("rtc_pi2rtc") + if result: + second_line = result.splitlines()[1] + status = second_line.split('rtc_pi2rtc: ')[1].strip() + if status == "done": + return True + return False From 87dbc0691b2c772d99403855bcadd21be664e41d Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 21:28:40 +0200 Subject: [PATCH 11/62] fix minor issue --- inkycal/utils/pisugar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 43fc4e3..e794f38 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -89,7 +89,7 @@ class PiSugar: result = self._get_output("get alarm_repeat") if result: second_line = result.splitlines()[1] - repeating_days = second_line.split('alarm_repeat: ')[1].strip() + repeating_days = f"{second_line.split('alarm_repeat: ')[1].strip():8b}".strip() data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False, "Saturday": False, "Sunday": False} if repeating_days[0] == "1": From 4112df0771894c37e8d45a91307e7850fd491d1d Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 21:30:30 +0200 Subject: [PATCH 12/62] fix minor issue --- inkycal/utils/pisugar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index e794f38..ab98abf 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -89,7 +89,7 @@ class PiSugar: result = self._get_output("get alarm_repeat") if result: second_line = result.splitlines()[1] - repeating_days = f"{second_line.split('alarm_repeat: ')[1].strip():8b}".strip() + repeating_days = f"{int(second_line.split('alarm_repeat: ')[1].strip()):8b}".strip() data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False, "Saturday": False, "Sunday": False} if repeating_days[0] == "1": From 862886be24c238c6aff974e4ed4763d07bae197d Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 21:35:38 +0200 Subject: [PATCH 13/62] fix small bug --- inkycal/utils/pisugar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index ab98abf..b0f0a82 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -61,7 +61,7 @@ class PiSugar: result = self._get_output("get rtc_time") if result: second_line = result.splitlines()[1] - return second_line.split('rtc_alarm_time: ')[1].strip() + return second_line.split('rtc_time: ')[1].strip() return None def get_rtc_alarm_enabled(self) -> str or None: From 8681e81e0016b22936f5351624917a6717b9916d Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 21:38:25 +0200 Subject: [PATCH 14/62] fix small bug --- inkycal/utils/pisugar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index b0f0a82..a1b1463 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -69,7 +69,8 @@ class PiSugar: result = self._get_output("get rtc_alarm_enabled") if result: second_line = result.splitlines()[1] - return second_line.split('rtc_alarm_enabled: ')[1].strip() + output = second_line.split('rtc_alarm_enabled: ')[1].strip() + return True if output == "true" else False return None def get_rtc_alarm_time(self) -> str or None: From 17c13e6d8e4030847666411c87312a19cf9bb7f1 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 23 Jun 2024 22:25:11 +0200 Subject: [PATCH 15/62] fix issue with yfinance --- inkycal/display/display.py | 2 +- inkycal/main.py | 14 +++++++++----- inkycal/modules/inkycal_stocks.py | 2 +- requirements.txt | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/inkycal/display/display.py b/inkycal/display/display.py index 179f71f..4eb47f3 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -197,7 +197,7 @@ class Display: >>> Display.get_display_names() """ - return supported_models.keys() + return list(supported_models.keys()) if __name__ == '__main__': diff --git a/inkycal/main.py b/inkycal/main.py index c1dd0f1..150e865 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -130,6 +130,8 @@ class Inkycal: self.cache = JSONCache(CACHE_NAME) self.cache_data = self.cache.read() + self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"]) + # Give an OK message print('loaded inkycal') @@ -259,8 +261,6 @@ class Inkycal: # Function to flip images upside down upside_down = lambda image: image.rotate(180, expand=True) - # Count the number of times without any errors - counter = 0 print(f'Inkycal version: v{self._release}') print(f'Selected E-paper display: {self.settings["model"]}') @@ -287,9 +287,9 @@ class Inkycal: if errors: logger.error("Error/s in modules:", *errors) - counter = 0 + self.counter = 0 else: - counter += 1 + self.counter += 1 logger.info("successful") del errors @@ -332,9 +332,13 @@ class Inkycal: (f"{self.image_folder}/canvas.png.hash", im_black),]): display.render(im_black) - print(f'\nNo errors since {counter} display updates \n' + print(f'\nNo errors since {self.counter} display updates \n' f'program started {runtime.humanize()}') + # store the cache data + self.cache.write(self.cache_data) + + # Exit the loop if run_once is True if run_once: break # Exit the loop after one full cycle if run_once is True diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index cf58a41..47a2b55 100755 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -142,7 +142,7 @@ class Stocks(inkycal_module): logger.warning(f"Failed to get '{stockName}' ticker price hint! Using " "default precision of 2 instead.") - stockHistory = yfTicker.history("30d") + stockHistory = yfTicker.history("1mo") stockHistoryLen = len(stockHistory) logger.info(f'fetched {stockHistoryLen} datapoints ...') previousQuote = (stockHistory.tail(2)['Close'].iloc[0]) diff --git a/requirements.txt b/requirements.txt index 3884b42..4554902 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,4 +51,4 @@ virtualenv==20.25.0 webencodings==0.5.1 x-wr-timezone==0.0.6 xkcd==2.4.2 -yfinance==0.2.36 +yfinance==0.2.40 From 310fefa1aead1e023c5b591eece0832245bd9d51 Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 01:53:52 +0200 Subject: [PATCH 16/62] small improvements --- inkycal/main.py | 4 ++-- inkycal/utils/pisugar.py | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index 150e865..b485ac4 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -81,10 +81,10 @@ class Inkycal: if self.render: # Init Display class with model in settings file # from inkycal.display import Display - self.Display = Display(settings["model"]) + self.Display = Display(self.settings["model"]) # check if colours can be rendered - self.supports_colour = True if 'colour' in settings['model'] else False + self.supports_colour = True if 'colour' in self.settings['model'] else False # get calibration hours self._calibration_hours = self.settings['calibration_hours'] diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index a1b1463..eb40da9 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -4,6 +4,7 @@ import logging import subprocess from inkycal.settings import Settings +import arrow settings = Settings() @@ -73,12 +74,13 @@ class PiSugar: return True if output == "true" else False return None - def get_rtc_alarm_time(self) -> str or None: + def get_rtc_alarm_time(self) -> arrow.arrow or None: """Get the RTC alarm time.""" result = self._get_output("get rtc_alarm_time") if result: second_line = result.splitlines()[1] - return second_line.split('rtc_alarm_time: ')[1].strip() + alarm_time = second_line.split('rtc_alarm_time: ')[1].strip() + return arrow.get(alarm_time) return None def get_alarm_repeat(self) -> dict or None: @@ -124,4 +126,22 @@ class PiSugar: return True return False + def rtc_alarm_set(self, time: arrow.arrow) -> bool: + """Set the RTC alarm time. + + Args: + time (arrow.arrow): The alarm time in ISO 8601 format. + + Returns: + bool: True if the alarm was set successfully, False otherwise. + """ + iso_format = time.isoformat() + result = self._get_output(f"rtc_alarm_set {iso_format}") + if result: + second_line = result.splitlines()[1] + status = second_line.split('rtc_alarm_set: ')[1].strip() + if status == "done": + return True + return False + From 36b5906534b4e7e209518814a9379933ee0061a2 Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 02:16:08 +0200 Subject: [PATCH 17/62] this and that --- inkycal/utils/pisugar.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index eb40da9..851bebd 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -35,7 +35,7 @@ class PiSugar: logger.error(f"Error executing command: {e}") return None - def get_battery(self) -> int or None: + def get_battery(self) -> float or None: """Get the battery level in percentage. Returns: @@ -45,7 +45,7 @@ class PiSugar: if battery_output: for line in battery_output.splitlines(): if 'battery:' in line: - return int(line.split(':')[1].strip()) + return float(line.split(':')[1].strip()) return None def get_model(self) -> str or None: @@ -57,12 +57,12 @@ class PiSugar: return line.split(':')[1].strip() return None - def get_rtc_time(self) -> str or None: + def get_rtc_time(self) -> arrow.arrow or None: """Get the RTC time.""" result = self._get_output("get rtc_time") if result: - second_line = result.splitlines()[1] - return second_line.split('rtc_time: ')[1].strip() + rtc_time = result.split("rtc_time: ")[1].strip() + return arrow.get(rtc_time) return None def get_rtc_alarm_enabled(self) -> str or None: @@ -78,8 +78,7 @@ class PiSugar: """Get the RTC alarm time.""" result = self._get_output("get rtc_alarm_time") if result: - second_line = result.splitlines()[1] - alarm_time = second_line.split('rtc_alarm_time: ')[1].strip() + alarm_time = result.split('rtc_alarm_time: ')[1].strip() return arrow.get(alarm_time) return None From ca8990cd0172ff68f7b79be7902410c14a7c2ceb Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 02:22:07 +0200 Subject: [PATCH 18/62] this and that --- inkycal/utils/pisugar.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 851bebd..681a9c8 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -17,7 +17,7 @@ class PiSugar: # replace "command" with actual command self.command_template = 'echo "command" | nc -q 0 127.0.0.1 8423' self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled", - "get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc"] + "get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc", "rtc_alarm_set"] def _get_output(self, command): if command not in self.allowed_commands: @@ -90,8 +90,7 @@ class PiSugar: """ result = self._get_output("get alarm_repeat") if result: - second_line = result.splitlines()[1] - repeating_days = f"{int(second_line.split('alarm_repeat: ')[1].strip()):8b}".strip() + repeating_days = f"{int(result.split('alarm_repeat: ')[1].strip()):8b}".strip() data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False, "Saturday": False, "Sunday": False} if repeating_days[0] == "1": From b6edc1ff0f11ac294b2b68b6a024fe8fc48e76ae Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 02:27:50 +0200 Subject: [PATCH 19/62] allow adding param --- inkycal/utils/pisugar.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 681a9c8..5ac0746 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -19,11 +19,14 @@ class PiSugar: self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled", "get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc", "rtc_alarm_set"] - def _get_output(self, command): + def _get_output(self, command, param=None): if command not in self.allowed_commands: logger.error(f"Command {command} not allowed") return None - cmd = self.command_template.replace("command", command) + if param: + cmd = self.command_template.replace("command", f"{command} {param}") + else: + cmd = self.command_template.replace("command", command) try: result = subprocess.run(cmd, shell=True, text=True, capture_output=True) if result.returncode != 0: @@ -134,7 +137,7 @@ class PiSugar: bool: True if the alarm was set successfully, False otherwise. """ iso_format = time.isoformat() - result = self._get_output(f"rtc_alarm_set {iso_format}") + result = self._get_output("rtc_alarm_set", iso_format) if result: second_line = result.splitlines()[1] status = second_line.split('rtc_alarm_set: ')[1].strip() From 617484bd807914d5bfcc789e10be611401f32e4a Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 02:41:52 +0200 Subject: [PATCH 20/62] fix minor bug --- inkycal/utils/pisugar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 5ac0746..5062643 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -127,17 +127,18 @@ class PiSugar: return True return False - def rtc_alarm_set(self, time: arrow.arrow) -> bool: + def rtc_alarm_set(self, time: arrow.arrow, repeat:int=127) -> bool: """Set the RTC alarm time. Args: time (arrow.arrow): The alarm time in ISO 8601 format. + repeat: int representing 7-bit binary number of repeating days. e.g. 127 = 1111111 = repeat every day Returns: bool: True if the alarm was set successfully, False otherwise. """ iso_format = time.isoformat() - result = self._get_output("rtc_alarm_set", iso_format) + result = self._get_output("rtc_alarm_set", f"{iso_format } {repeat}") if result: second_line = result.splitlines()[1] status = second_line.split('rtc_alarm_set: ')[1].strip() From bbe9498d9d9be53822c4dbf5d02478de3cd3c198 Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 02:47:21 +0200 Subject: [PATCH 21/62] fix output of rtc_alarm_set --- inkycal/utils/pisugar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 5062643..48cdf8d 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -140,8 +140,7 @@ class PiSugar: iso_format = time.isoformat() result = self._get_output("rtc_alarm_set", f"{iso_format } {repeat}") if result: - second_line = result.splitlines()[1] - status = second_line.split('rtc_alarm_set: ')[1].strip() + status = result.split('rtc_alarm_set: ')[1].strip() if status == "done": return True return False From 98e3efa14bac87eb6ace694acff630cca747b0fd Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 02:59:48 +0200 Subject: [PATCH 22/62] ad-hoc implementation for pisugar in main class --- inkycal/main.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/inkycal/main.py b/inkycal/main.py index b485ac4..e289d5e 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -132,6 +132,14 @@ class Inkycal: self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"]) + self.use_pi_sugar = True + print("Please remove hardcoded value of pisugar!") + + if self.use_pi_sugar: + from inkycal.utils import PiSugar + self.pisugar = PiSugar() + print(f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}") + # Give an OK message print('loaded inkycal') @@ -343,6 +351,16 @@ class Inkycal: break # Exit the loop after one full cycle if run_once is True sleep_time = self.countdown() + + if self.use_pi_sugar: + # todo make this timezone aware! + sleep_time = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time) + result = self.pisugar.rtc_alarm_set(sleep_time, 127) + if result: + print(f"Alarm set for {sleep_time.format('HH:mm:ss')}") + else: + print(f"Failed to set alarm for {sleep_time.format('HH:mm:ss')}") + await asyncio.sleep(sleep_time) @staticmethod From 92ef8476183ef676415a8823c4a04a7d728e12be Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 24 Jun 2024 03:21:40 +0200 Subject: [PATCH 23/62] fix minor issue --- inkycal/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index e289d5e..453a4a4 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -354,12 +354,12 @@ class Inkycal: if self.use_pi_sugar: # todo make this timezone aware! - sleep_time = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time) - result = self.pisugar.rtc_alarm_set(sleep_time, 127) + sleep_time_rtc = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time) + result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127) if result: - print(f"Alarm set for {sleep_time.format('HH:mm:ss')}") + print(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}") else: - print(f"Failed to set alarm for {sleep_time.format('HH:mm:ss')}") + print(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}") await asyncio.sleep(sleep_time) From fdd5591456b922896bda1862640dc240ed0639c6 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 25 Jun 2024 13:17:00 +0200 Subject: [PATCH 24/62] add some nice-to-have features, e.g. battery capacity checking of PiSugar --- inkycal/main.py | 58 +++++++++++++++++++++++++++++++--------- inkycal/utils/pisugar.py | 3 +-- tests/test_main.py | 4 +-- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index 453a4a4..64eecad 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -37,15 +37,19 @@ 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, use_pi_sugar: bool = False): """Initialise Inkycal""" self._release = "2.0.3" + logger.info(f"Inkycal v{self._release} booting up...") + self.render = render self.info = None + logger.info("Checking if a settings file is present...") # load settings file - throw an error if file could not be found if settings_path: + logger.info(f"Custom location for settings.json file specified: {settings_path}") try: with open(settings_path) as settings_file: self.settings = json.load(settings_file) @@ -55,6 +59,7 @@ class Inkycal: f"No settings.json file could be found in the specified location: {settings_path}") else: + logger.info("Looking for settings.json file in /boot folder...") try: with open('/boot/settings.json', mode="r") as settings_file: self.settings = json.load(settings_file) @@ -63,6 +68,8 @@ class Inkycal: raise SettingsFileNotFoundError self.disable_calibration = self.settings.get('disable_calibration', False) + if self.disable_calibration: + logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.") if not os.path.exists(settings.IMAGE_FOLDER): os.mkdir(settings.IMAGE_FOLDER) @@ -132,16 +139,31 @@ class Inkycal: self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"]) - self.use_pi_sugar = True - print("Please remove hardcoded value of pisugar!") + self.use_pi_sugar = use_pi_sugar + self.battery_capacity = 100 if self.use_pi_sugar: + logger.info("PiSugar support enabled.") from inkycal.utils import PiSugar self.pisugar = PiSugar() - print(f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}") + + self.battery_capacity = self.pisugar.get_battery() + logger.info(f"PiSugar battery capacity: {self.battery_capacity}%") + + if self.battery_capacity < 20: + logger.warning("Battery capacity is below 20%!") + + logger.info("Setting system time to PiSugar time...") + if self.pisugar.rtc_pi2rtc(): + logger.info("RTC time updates successfully") + else: + logger.warning("RTC time could not be set!") + + print( + f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}") # Give an OK message - print('loaded inkycal') + logger.info('Inkycal initialised successfully!') def countdown(self, interval_mins: int = None) -> int: """Returns the remaining time in seconds until the next display update based on the interval. @@ -179,7 +201,7 @@ class Inkycal: return seconds_to_next_interval - def test(self): + def dry_run(self): """Tests if Inkycal can run without issues. Attempts to import module names from settings file. Loads the config @@ -269,7 +291,6 @@ class Inkycal: # Function to flip images upside down upside_down = lambda image: image.rotate(180, expand=True) - print(f'Inkycal version: v{self._release}') print(f'Selected E-paper display: {self.settings["model"]}') @@ -291,7 +312,7 @@ class Inkycal: success = self.process_module(number) if not success: errors.append(number) - self.info += f"module {number}: Error! " + self.info += f"im {number}: X " if errors: logger.error("Error/s in modules:", *errors) @@ -301,6 +322,9 @@ class Inkycal: logger.info("successful") del errors + if self.battery_capacity < 20: + self.info += "Low battery! " + # Assemble image from each module - add info section if specified self._assemble() @@ -337,7 +361,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) print(f'\nNo errors since {self.counter} display updates \n' @@ -353,13 +377,12 @@ class Inkycal: sleep_time = self.countdown() if self.use_pi_sugar: - # todo make this timezone aware! sleep_time_rtc = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time) result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127) if result: - print(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}") + logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}") else: - print(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}") + logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}") await asyncio.sleep(sleep_time) @@ -575,6 +598,17 @@ class Inkycal: logger.exception(f"Error in module {number}!") return False + def _shutdown_system(self): + """Shutdown the system""" + import subprocess + from time import sleep + try: + logger.info("Shutting down OS in 5 seconds...") + sleep(5) + subprocess.run(["sudo", "shutdown", "-h", "now"], check=True) + except subprocess.CalledProcessError: + logger.warning("Failed to execute shutdown command.") + if __name__ == '__main__': print(f'running inkycal main in standalone/debug mode') diff --git a/inkycal/utils/pisugar.py b/inkycal/utils/pisugar.py index 48cdf8d..0c89911 100644 --- a/inkycal/utils/pisugar.py +++ b/inkycal/utils/pisugar.py @@ -121,8 +121,7 @@ class PiSugar: """ result = self._get_output("rtc_pi2rtc") if result: - second_line = result.splitlines()[1] - status = second_line.split('rtc_pi2rtc: ')[1].strip() + status = result.split('rtc_pi2rtc: ')[1].strip() if status == "done": return True return False diff --git a/tests/test_main.py b/tests/test_main.py index ceb834c..e8c30af 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -21,9 +21,9 @@ class TestMain(unittest.TestCase): assert inkycal.settings["info_section_height"] == 70 assert inkycal.settings["border_around_modules"] is True - def test_run(self): + def test_dry_run(self): inkycal = Inkycal(self.settings_path, render=False) - inkycal.test() + inkycal.dry_run() def test_countdown(self): inkycal = Inkycal(self.settings_path, render=False) From 427c55ef63b9ed8822b043011d6098075fe3e853 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 25 Jun 2024 13:39:33 +0200 Subject: [PATCH 25/62] async and logging improvements, minor code cleanup --- clear_display.py | 12 ----------- inky_run.py | 33 +++++++++++++++++++++++++++---- inkycal/loggers.py | 2 +- inkycal/modules/inkycal_agenda.py | 4 +--- 4 files changed, 31 insertions(+), 20 deletions(-) delete mode 100644 clear_display.py diff --git a/clear_display.py b/clear_display.py deleted file mode 100644 index be726f3..0000000 --- a/clear_display.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Clears the display of any content. -""" -from inkycal import Inkycal - -print("loading Inkycal and display driver...") -inky = Inkycal(render=True) # Initialise Inkycal -print("clearing display...") -inky.calibrate(cycles=1) # Calibrate the display -print("clear complete...") - -print("finished!") diff --git a/inky_run.py b/inky_run.py index 2fb9718..9f52494 100644 --- a/inky_run.py +++ b/inky_run.py @@ -4,9 +4,34 @@ Assumes that the settings.json file is in the /boot directory. set render=True to render the display, set render=False to only run the modules. """ import asyncio + from inkycal import Inkycal -inky = Inkycal(render=True) # Initialise Inkycal -# If your settings.json file is not in /boot, use the full path: inky = Inkycal('path/to/settings.json', render=True) -inky.run(run_once=True) # test if Inkycal can be run correctly, running this will show a bit of info for each module -asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop + +async def dry_run(): + # create an instance of Inkycal + # If your settings.json file is not in /boot, use the full path: + # inky = Inkycal('path/to/settings.json', render=True) + inky = Inkycal(render=False) + await inky.run(run_once=True) # dry-run without rendering anything on the display + + +async def clear_display(): + print("loading Inkycal and display driver...") + inky = Inkycal(render=True) # Initialise Inkycal + print("clearing display...") + inky.calibrate(cycles=1) # Calibrate the display + print("clear complete...") + print("finished!") + + +async def run(): + # create an instance of Inkycal + # If your settings.json file is not in /boot, use the full path: + # inky = Inkycal('path/to/settings.json', render=True) + inky = Inkycal(render=True) + await inky.run() # If there were no issues, you can run Inkycal nonstop + + +if __name__ == "__main__": + asyncio.run(run()) diff --git a/inkycal/loggers.py b/inkycal/loggers.py index 97c1c46..3a59c33 100644 --- a/inkycal/loggers.py +++ b/inkycal/loggers.py @@ -8,7 +8,7 @@ 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) +stream_handler.setLevel(logging.INFO) settings = Settings() diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 5b7f5d0..813a3db 100755 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -2,9 +2,7 @@ Inkycal Agenda Module Copyright by aceinnolab """ - -import arrow - +import arrow # noqa from inkycal.custom import * from inkycal.modules.ical_parser import iCalendar from inkycal.modules.template import inkycal_module From 762538d4da427ae98371db8196b1de4fe46bf01b Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 25 Jun 2024 14:22:38 +0200 Subject: [PATCH 26/62] minor logging improvements --- inky_run.py | 3 +++ inkycal/main.py | 2 +- inkycal/modules/inky_image.py | 2 +- inkycal/modules/inkycal_agenda.py | 2 +- inkycal/modules/inkycal_feeds.py | 5 +++-- inkycal/modules/inkycal_image.py | 2 +- inkycal/modules/inkycal_jokes.py | 5 +++-- inkycal/modules/inkycal_slideshow.py | 2 +- inkycal/modules/inkycal_stocks.py | 2 +- inkycal/modules/inkycal_textfile_to_display.py | 2 +- inkycal/modules/inkycal_tindie.py | 3 ++- inkycal/modules/inkycal_todoist.py | 3 ++- inkycal/modules/inkycal_weather.py | 5 +++-- inkycal/modules/inkycal_webshot.py | 3 ++- inkycal/modules/inkycal_xkcd.py | 3 ++- 15 files changed, 27 insertions(+), 17 deletions(-) diff --git a/inky_run.py b/inky_run.py index 9f52494..9f4d5f7 100644 --- a/inky_run.py +++ b/inky_run.py @@ -29,6 +29,9 @@ async def run(): # create an instance of Inkycal # If your settings.json file is not in /boot, use the full path: # inky = Inkycal('path/to/settings.json', render=True) + + # when using experimental PiSugar support: + # inky = Inkycal(render=True, use_pi_sugar=True) inky = Inkycal(render=True) await inky.run() # If there were no issues, you can run Inkycal nonstop diff --git a/inkycal/main.py b/inkycal/main.py index 64eecad..8b4a26f 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -319,7 +319,7 @@ class Inkycal: self.counter = 0 else: self.counter += 1 - logger.info("successful") + logger.info("All images generated successfully!") del errors if self.battery_capacity < 20: diff --git a/inkycal/modules/inky_image.py b/inkycal/modules/inky_image.py index 67f4a14..eaefe28 100755 --- a/inkycal/modules/inky_image.py +++ b/inkycal/modules/inky_image.py @@ -59,7 +59,7 @@ class Inkyimage: logger.error("Invalid Image file provided", exc_info=True) raise Exception("Please check if the path points to an image file.") - logger.info(f"width: {image.width}, height: {image.height}") + logger.debug(f"width: {image.width}, height: {image.height}") image.convert(mode="RGBA") # convert to a more suitable format self.image = image diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 813a3db..5b27d00 100755 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -86,7 +86,7 @@ class Agenda(inkycal_module): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') diff --git a/inkycal/modules/inkycal_feeds.py b/inkycal/modules/inkycal_feeds.py index d7bdde3..a51eba6 100644 --- a/inkycal/modules/inkycal_feeds.py +++ b/inkycal/modules/inkycal_feeds.py @@ -75,7 +75,7 @@ class Feeds(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -83,8 +83,9 @@ class Feeds(inkycal_module): # Check if internet is available if internet_available(): - logger.info('Connection test passed') + logger.debug('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting feeds diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index 5387b9b..bb7558b 100755 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -71,7 +71,7 @@ class Inkyimage(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) diff --git a/inkycal/modules/inkycal_jokes.py b/inkycal/modules/inkycal_jokes.py index 5f0085e..004037b 100755 --- a/inkycal/modules/inkycal_jokes.py +++ b/inkycal/modules/inkycal_jokes.py @@ -39,7 +39,7 @@ class Jokes(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'image size: {im_width} x {im_height} px') + logger.debug(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -47,8 +47,9 @@ class Jokes(inkycal_module): # Check if internet is available if internet_available(): - logger.info('Connection test passed') + logger.debug('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting feeds diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index 253397d..8ac612e 100755 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -87,7 +87,7 @@ class Slideshow(inkycal_module): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # rotates list items by 1 index def rotate(list: list): diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index 47a2b55..c82506f 100755 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -63,7 +63,7 @@ class Stocks(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'image size: {im_width} x {im_height} px') + logger.debug(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size=im_size, color='white') diff --git a/inkycal/modules/inkycal_textfile_to_display.py b/inkycal/modules/inkycal_textfile_to_display.py index 7dc4987..1f5b76e 100644 --- a/inkycal/modules/inkycal_textfile_to_display.py +++ b/inkycal/modules/inkycal_textfile_to_display.py @@ -45,7 +45,7 @@ class TextToDisplay(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') diff --git a/inkycal/modules/inkycal_tindie.py b/inkycal/modules/inkycal_tindie.py index 24b51cb..c07efa0 100755 --- a/inkycal/modules/inkycal_tindie.py +++ b/inkycal/modules/inkycal_tindie.py @@ -40,7 +40,7 @@ class Tindie(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'image size: {im_width} x {im_height} px') + logger.debug(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -50,6 +50,7 @@ class Tindie(inkycal_module): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting feeds diff --git a/inkycal/modules/inkycal_todoist.py b/inkycal/modules/inkycal_todoist.py index 55e725e..3d41619 100644 --- a/inkycal/modules/inkycal_todoist.py +++ b/inkycal/modules/inkycal_todoist.py @@ -70,7 +70,7 @@ class Todoist(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -80,6 +80,7 @@ class Todoist(inkycal_module): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError # Set some parameters for formatting todos diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 7aeb845..e6b747b 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -154,7 +154,7 @@ class Weather(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') @@ -162,8 +162,9 @@ class Weather(inkycal_module): # Check if internet is available if internet_available(): - logger.info('Connection test passed') + logger.debug('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise NetworkNotReachableError def get_moon_phase(): diff --git a/inkycal/modules/inkycal_webshot.py b/inkycal/modules/inkycal_webshot.py index 8d08cc6..f4945f6 100644 --- a/inkycal/modules/inkycal_webshot.py +++ b/inkycal/modules/inkycal_webshot.py @@ -90,7 +90,7 @@ class Webshot(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.debug('image size: {} x {} px'.format(im_width, im_height)) # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size=im_size, color='white') @@ -100,6 +100,7 @@ class Webshot(inkycal_module): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise Exception('Network could not be reached :/') logger.info( diff --git a/inkycal/modules/inkycal_xkcd.py b/inkycal/modules/inkycal_xkcd.py index 63f7140..f3325d8 100644 --- a/inkycal/modules/inkycal_xkcd.py +++ b/inkycal/modules/inkycal_xkcd.py @@ -68,7 +68,7 @@ class Xkcd(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.debug('image size: {} x {} px'.format(im_width, im_height)) # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size=im_size, color='white') @@ -78,6 +78,7 @@ class Xkcd(inkycal_module): if internet_available(): logger.info('Connection test passed') else: + logger.error("Network not reachable. Please check your connection.") raise Exception('Network could not be reached :/') # Set some parameters for formatting feeds From d365090c3b597fc01db37e42b5ed5a827304af47 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 25 Jun 2024 14:27:54 +0200 Subject: [PATCH 27/62] use logging in favour of print --- inkycal/modules/dev_module.py | 2 +- inkycal/modules/inky_image.py | 2 +- inkycal/modules/inkycal_agenda.py | 2 +- inkycal/modules/inkycal_calendar.py | 4 ++-- inkycal/modules/inkycal_feeds.py | 2 +- inkycal/modules/inkycal_fullweather.py | 2 +- inkycal/modules/inkycal_image.py | 2 +- inkycal/modules/inkycal_jokes.py | 2 +- inkycal/modules/inkycal_server.py | 2 +- inkycal/modules/inkycal_slideshow.py | 2 +- inkycal/modules/inkycal_stocks.py | 2 +- inkycal/modules/inkycal_textfile_to_display.py | 2 +- inkycal/modules/inkycal_tindie.py | 2 +- inkycal/modules/inkycal_todoist.py | 2 +- inkycal/modules/inkycal_weather.py | 2 +- inkycal/modules/inkycal_webshot.py | 2 +- inkycal/modules/inkycal_xkcd.py | 2 +- 17 files changed, 18 insertions(+), 18 deletions(-) diff --git a/inkycal/modules/dev_module.py b/inkycal/modules/dev_module.py index 13d6275..53d8db3 100755 --- a/inkycal/modules/dev_module.py +++ b/inkycal/modules/dev_module.py @@ -156,7 +156,7 @@ class Simple(inkycal_module): # -----------------------------------------------------------------------# # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') ############################################################################# # Validation of module specific parameters (optional) # diff --git a/inkycal/modules/inky_image.py b/inkycal/modules/inky_image.py index eaefe28..5795f0e 100755 --- a/inkycal/modules/inky_image.py +++ b/inkycal/modules/inky_image.py @@ -27,7 +27,7 @@ class Inkyimage: self.image = image # give an OK message - logger.info(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") def load(self, path: str) -> None: """loads an image from a URL or filepath. diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 5b27d00..a91f93b 100755 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -76,7 +76,7 @@ class Agenda(inkycal_module): self.timezone = get_system_tz() # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index 8d0cd50..0f947cd 100755 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -84,7 +84,7 @@ class Calendar(inkycal_module): ) # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') @staticmethod def flatten(values): @@ -100,7 +100,7 @@ class Calendar(inkycal_module): im_size = im_width, im_height events_height = 0 - logger.info(f'Image size: {im_size}') + logger.debug(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size=im_size, color='white') diff --git a/inkycal/modules/inkycal_feeds.py b/inkycal/modules/inkycal_feeds.py index a51eba6..38a6294 100644 --- a/inkycal/modules/inkycal_feeds.py +++ b/inkycal/modules/inkycal_feeds.py @@ -60,7 +60,7 @@ class Feeds(inkycal_module): self.shuffle_feeds = config["shuffle_feeds"] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def _validate(self): """Validate module-specific parameters""" diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py index e48f17f..5dccc70 100644 --- a/inkycal/modules/inkycal_fullweather.py +++ b/inkycal/modules/inkycal_fullweather.py @@ -239,7 +239,7 @@ class Fullweather(inkycal_module): self.left_section_width = int(self.width / 4) # give an OK message - print(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") def createBaseImage(self): """ diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index bb7558b..bdf94bd 100755 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -50,7 +50,7 @@ class Inkyimage(inkycal_module): self.dither = False # give an OK message - print(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_jokes.py b/inkycal/modules/inkycal_jokes.py index 004037b..e35fd5e 100755 --- a/inkycal/modules/inkycal_jokes.py +++ b/inkycal/modules/inkycal_jokes.py @@ -30,7 +30,7 @@ class Jokes(inkycal_module): config = config['config'] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_server.py b/inkycal/modules/inkycal_server.py index 619c146..4cade83 100755 --- a/inkycal/modules/inkycal_server.py +++ b/inkycal/modules/inkycal_server.py @@ -67,7 +67,7 @@ class Inkyserver(inkycal_module): self.path_body = config['path_body'] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index 8ac612e..926d72b 100755 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -77,7 +77,7 @@ class Slideshow(inkycal_module): self._first_run = True # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index c82506f..4e7538f 100755 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -54,7 +54,7 @@ class Stocks(inkycal_module): self.tickers = config['tickers'] # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_textfile_to_display.py b/inkycal/modules/inkycal_textfile_to_display.py index 1f5b76e..dc36ad1 100644 --- a/inkycal/modules/inkycal_textfile_to_display.py +++ b/inkycal/modules/inkycal_textfile_to_display.py @@ -31,7 +31,7 @@ class TextToDisplay(inkycal_module): self.make_request = True if self.filepath.startswith("https://") else False # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def _validate(self): """Validate module-specific parameters""" diff --git a/inkycal/modules/inkycal_tindie.py b/inkycal/modules/inkycal_tindie.py index c07efa0..bf2b92b 100755 --- a/inkycal/modules/inkycal_tindie.py +++ b/inkycal/modules/inkycal_tindie.py @@ -32,7 +32,7 @@ class Tindie(inkycal_module): # self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_todoist.py b/inkycal/modules/inkycal_todoist.py index 3d41619..0e95585 100644 --- a/inkycal/modules/inkycal_todoist.py +++ b/inkycal/modules/inkycal_todoist.py @@ -56,7 +56,7 @@ class Todoist(inkycal_module): self._api = TodoistAPI(config['api_key']) # give an OK message - print(f'{__name__} loaded') + logger.debug(f'{__name__} loaded') def _validate(self): """Validate module-specific parameters""" diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index e6b747b..8bfe2c2 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -143,7 +143,7 @@ class Weather(inkycal_module): self.tempDispUnit = "°" # give an OK message - print(f"{__name__} loaded") + logger.debug(f"{__name__} loaded") diff --git a/inkycal/modules/inkycal_webshot.py b/inkycal/modules/inkycal_webshot.py index f4945f6..ae49dee 100644 --- a/inkycal/modules/inkycal_webshot.py +++ b/inkycal/modules/inkycal_webshot.py @@ -74,7 +74,7 @@ class Webshot(inkycal_module): self.crop_y = 0 # give an OK message - print(f'Inkycal webshot loaded') + logger.debug(f'Inkycal webshot loaded') def generate_image(self): """Generate image for this module""" diff --git a/inkycal/modules/inkycal_xkcd.py b/inkycal/modules/inkycal_xkcd.py index f3325d8..864e15c 100644 --- a/inkycal/modules/inkycal_xkcd.py +++ b/inkycal/modules/inkycal_xkcd.py @@ -53,7 +53,7 @@ class Xkcd(inkycal_module): self.scale_filter = config['filter'] # give an OK message - print(f'Inkycal XKCD loaded') + logger.debug(f'Inkycal XKCD loaded') def generate_image(self): """Generate image for this module""" From 79a9b88091354bf429faabd2d6583efe62bc2591 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 25 Jun 2024 14:40:12 +0200 Subject: [PATCH 28/62] logging improvements --- inkycal/main.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index 8b4a26f..476f683 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -210,8 +210,6 @@ class Inkycal: Generated images can be found in the /images folder of Inkycal. """ - - logger.info(f"Inkycal version: v{self._release}") logger.info(f'Selected E-paper display: {self.settings["model"]}') # store module numbers in here @@ -222,11 +220,11 @@ class Inkycal: for number in range(1, self._module_number): name = eval(f"self.module_{number}.name") - print(f'generating image(s) for {name}...', end="") success = self.process_module(number) if success: - print("OK!") + logger.debug(f'Image of module {name} generated successfully') else: + logger.warning(f'Generating image of module {name} failed!') errors.append(number) self.info += f"module {number}: Error! " @@ -291,14 +289,14 @@ class Inkycal: # Function to flip images upside down upside_down = lambda image: image.rotate(180, expand=True) - print(f'Inkycal version: v{self._release}') - print(f'Selected E-paper display: {self.settings["model"]}') + logger.info(f'Inkycal version: v{self._release}') + logger.info(f'Selected E-paper display: {self.settings["model"]}') while True: + logger.info("Starting new cycle...") current_time = arrow.now(tz=get_system_tz()) - print(f"Date: {current_time.format('D MMM YY')} | " - f"Time: {current_time.format('HH:mm')}") - print('Generating images for all modules...', end='') + logger.info(f"Timestamp: {current_time.format('HH:mm:ss DD.MM.YYYY')}") + self.cache_data["counter"] = self.counter errors = [] # Store module numbers in here @@ -317,8 +315,10 @@ class Inkycal: if errors: logger.error("Error/s in modules:", *errors) self.counter = 0 + self.cache_data["counter"] = 0 else: self.counter += 1 + self.cache_data["counter"] += 1 logger.info("All images generated successfully!") del errors @@ -330,6 +330,7 @@ class Inkycal: # Check if image should be rendered if self.render: + logger.info("Attempting to render image on display...") display = self.Display self._calibration_check() if self._calibration_state: @@ -353,7 +354,7 @@ class Inkycal: display.render(im_black, im_colour) # Part for black-white ePapers - elif not self.supports_colour: + else: im_black = self._merge_bands() # Flip the image by 180° if required @@ -364,8 +365,8 @@ class Inkycal: (f"{self.image_folder}/canvas.png.hash", im_black), ]): display.render(im_black) - print(f'\nNo errors since {self.counter} display updates \n' - f'program started {runtime.humanize()}') + logger.info(f'\nNo errors since {self.counter} display updates') + logger.info(f'program started {runtime.humanize()}') # store the cache data self.cache.write(self.cache_data) From a02a3ffdee21a08de507ee1c56705569ed6be91c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Jun 2024 17:14:45 +0000 Subject: [PATCH 29/62] update docs [bot] --- docs/genindex.html | 12 ++++++---- docs/inkycal.html | 53 ++++++++++++++++++++++++++------------------ docs/objects.inv | Bin 634 -> 646 bytes docs/searchindex.js | 2 +- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/docs/genindex.html b/docs/genindex.html index 4bca78f..be82be7 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -129,6 +129,10 @@ +
@@ -253,6 +257,10 @@ +
@@ -285,10 +293,6 @@

T

-
@@ -130,7 +131,7 @@ Copyright by aceinnolab

-class inkycal.main.Inkycal(settings_path: str = None, render: bool = True)
+class inkycal.main.Inkycal(settings_path: str = None, render: bool = True, use_pi_sugar: bool = False)

Inkycal main class

Main class of Inkycal, test and run the main Inkycal program.

@@ -157,35 +158,21 @@ cycles. After a refresh cycle, a new image is generated and shown.

countdown(interval_mins: int = None) int
-

Returns the remaining time in seconds until next display update.

+

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.

    +
    Args:
    +
    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

  • -
+
Returns:

int: The remaining time in seconds until the next update.

-
-async run()
-

Runs main program in nonstop mode.

-

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.

-
- -
-
-test()
+
+dry_run()

Tests if Inkycal can run without issues.

Attempts to import module names from settings file. Loads the config for each module and initializes the module. Tries to run the module and @@ -193,6 +180,28 @@ checks if the images could be generated correctly.

Generated images can be found in the /images folder of Inkycal.

+
+
+process_module(number) bool
+

Process individual module to generate images and handle exceptions.

+
+ +
+
+async run(run_once=False)
+

Runs main program in nonstop mode or a single iteration based on the run_once flag.

+
+
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.

+
+
diff --git a/docs/objects.inv b/docs/objects.inv index df0d12b84eda2aa56ec36add55bb97cc65ffa7c3..e54dc411ad5904efd6d0c696c83bd3342a7cd7b2 100644 GIT binary patch delta 538 zcmV+#0_FYs1cn8WcYn=p+b|5r?|upc+cm~|?X6fZJq+lu2M`#Gj)m%Llw{X^`pTA* z*jSf1jt}_|#s5!~L`o9+tt&wmWpLi8q6)h7!f2PALg)&6i(2xy&;aYnsGp%0=bLeI)CR;G5@ieBD6=+5d{qh z346JISvOl zp|1^Jt~Eh>Zd$!U%c)}&)+jBR5}jk?0^JT+KI||;OvKn=7RaTdTD~*?>I?A`Bx(+{ zY^yF23XqcVF@L?*OlE>AB?2*Qa4h?KdHo5nEp2w5+zXTjIIGAgR*A(qbe$MTZozW3 zX*XOwXUN~kiESnn9B^H}pX5W$oj}Xe{R_ZwK~_@OwNsg(hHT$}vr#yG?reZjNC(`2 zV+k99?FJ-5Wg284Z4H~?j&Gw?py8XvSC);H;jiE3uzxqf=g#v-ZpwW3kPn8TKYtqf z#v3K$*s$Jb;yap>_j4>}X>7bcqA93Y*w7D}9Z(C+@qG}+7Y@o^dbB3(OaC`JyN&#r zN4opx>~T7C&3T5W*yBQd5rge^$ByLwD;)wUnn~{!*=rX!O3&4QgDBse2bwMJ*_xEU cPwmXbY7;H%`AEr@T>qs0*SNjeC%q;#YlkonYybcN delta 526 zcmV+p0`dKZ1^NV#cYn!l+%OD=?|upc?RAXinp@Faaww3Z4wrsBsMQ?+N{WC~}joeUU-?tmFM)?p{G&p>jhOot4l zZ6OQpc(2(6n!Z`QA=%g&^!Z5)dlRVi#2>k-%iTjc8Gokv^7}M5A1MXLj*UJSzOyNL zKgDL2#>V3#nu3ar4dbH40kzN^-zH(aouC}0M{CkPkAI7^+st2hq;q@8p0~5qoOgJN zJtpdd80@e+P9*oA=_Qb&xtRS#_A Date: Tue, 25 Jun 2024 19:38:28 +0200 Subject: [PATCH 30/62] create cache directory if not found --- inkycal/utils/json_cache.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/inkycal/utils/json_cache.py b/inkycal/utils/json_cache.py index 76e8e19..dda173a 100644 --- a/inkycal/utils/json_cache.py +++ b/inkycal/utils/json_cache.py @@ -12,6 +12,10 @@ 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 not os.path.exists(settings.CACHE_PATH): + os.makedirs(settings.CACHE_PATH) + 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) From 758d748d762f034cdd7bd0a682933bb439b71c30 Mon Sep 17 00:00:00 2001 From: Ace Date: Wed, 26 Jun 2024 01:47:58 +0200 Subject: [PATCH 31/62] prepare for release 2.0.4 --- .github/workflows/test-on-rpi.yml | 2 +- .github/workflows/update-os.yml | 2 +- Changelog.md | 42 ++++++++++++++++++++++++++++--- README.md | 2 +- docsource/conf.py | 2 +- inky_run.py | 37 ++++++++++++++------------- inkycal/display/display.py | 2 +- inkycal/main.py | 2 +- setup.py | 4 +-- 9 files changed, 66 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test-on-rpi.yml b/.github/workflows/test-on-rpi.yml index 5e3f014..42a1c12 100644 --- a/.github/workflows/test-on-rpi.yml +++ b/.github/workflows/test-on-rpi.yml @@ -42,7 +42,7 @@ jobs: whoami cd /home/inky sudo apt-get update -y - sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev -y + sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-base libopenblas-dev -y echo $PWD && ls git clone https://github.com/aceinnolab/Inkycal cd Inkycal diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 8acb810..32e34bc 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -40,7 +40,7 @@ jobs: # sudo apt-get dist-upgrade -y sudo apt-get install -y python3-pip - sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev libxml2-dev libxslt-dev python-dev-is-python3 -y + sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-base libopenblas-dev libxml2-dev libxslt-dev python-dev-is-python3 -y # #334 & #335 git clone https://github.com/WiringPi/WiringPi cd WiringPi diff --git a/Changelog.md b/Changelog.md index cc5fc69..e817e02 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,13 +1,47 @@ -# E-Paper-Calendar Software Changelog +# Inkycal Software Changelog All significant changes will be documented in this file. The order is from latest to oldest and structured in the following way: * Version name with date of publishing -* Sections with either 'added', 'fixed', 'updated' and 'changed' +* Sections with either 'added', 'fixed', 'updated', 'changed' or 'removed' to describe the changes ## [2.0.3] 2024 +### Changed +- Updated dependencies to the most-recent supported version +- Unified logging all over the library. Print statements are now rare. This makes it easier to identify why Inkycal isn't working without having to look up the logs +- Inkycal now makes use of a JSON-Cache to make it more resilient against resets etc. For example, the slideshow module will remember the last index even after a shutdown +- Inkycal now uses a list of supported displays instead of having to look up each driver in the driver directory +- Renamed tests according to python standards, starting with `test_..`, allowing unittest/pytest to automatically discover and run these tests. + +### Fixed +- Fixed an annoying vertical alignment issue causing some characters to look chopped off +- Fixed the alignment of the red-circle on the calendar module +- Fixed weekday-names not translating in the weather module +- Fixed python 3.11 issues with numpy on Raspberry Pi OS + ### Added -* Added fullscreen weather module -* Own OWM API abstraction as a replacement for PyOWM module +- Added long-awaited support of PiSugar v1/2/3. Still a bit experimental (no calibration handling), but works for most part. If PiSugar support is enabled, Inkycal will set the new alarm before shutting down the system, increasing battery life. Please note that around 70 updates were possible with the 1200mAh PiSugar 3 board, so one update a day to three should be max to get at least one month battery life. +- Added Webshot module which can be used to display a webpage. Works on InkycalOS-Lite too and does not need a GUI. +- Added XKCD module +- Added Tindie module +- Added support for much longer update-intervals than the previous max of once every 60 minutes +- Added Material-UI icons font +- Added dedicated Pipeline for unittests directly on Raspberry Pi OS to ensure Inkycal can run reliably on Raspberry Pi OS +- Added Feature-request and PR template +- Added support for 5.83" display (v2) +- Added support for 12.48" display on 64-bit systems +- Added Inkycal fullweather-module +- Added `settings.py file (not to be confused with `settings.json`) to set VCOM and other internal variables + + +## [2.0.3] 2023 +### Changed +- Switched from pyowm to custom wrapper as pyowm only works up to python3.9, which is now outdated. +- Updated dependencies to the most-recent supported version + +### Fixed +- Fixed python 3.11 issues with numpy on Raspberry Pi OS +- Fixed compatibility issues with Pillow when switching from v9.x to v10.x, particularly font width and height operations +- Renamed tests according to python standards, starting with `test_..`, allowing unittest/pytest to automatically discover and run these tests. ## [2.0.2] 2022 diff --git a/README.md b/README.md index 6663b97..42fde12 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Run the following steps to install Inkycal. Do **not** use sudo for this, except ```bash # Raspberry Pi specific section start -sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev +sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-base libopenblas-dev git clone https://github.com/WiringPi/WiringPi cd WiringPi ./build diff --git a/docsource/conf.py b/docsource/conf.py index 790c661..b4e96aa 100644 --- a/docsource/conf.py +++ b/docsource/conf.py @@ -22,7 +22,7 @@ copyright = '2018-2023, aceinnolab' author = 'aceinnolab' # The full version, including alpha/beta/rc tags -release = '2.0.3' +release = '2.0.4' # -- General configuration --------------------------------------------------- diff --git a/inky_run.py b/inky_run.py index 9f4d5f7..711b2dc 100644 --- a/inky_run.py +++ b/inky_run.py @@ -8,24 +8,8 @@ import asyncio from inkycal import Inkycal -async def dry_run(): - # create an instance of Inkycal - # If your settings.json file is not in /boot, use the full path: - # inky = Inkycal('path/to/settings.json', render=True) - inky = Inkycal(render=False) - await inky.run(run_once=True) # dry-run without rendering anything on the display - - -async def clear_display(): - print("loading Inkycal and display driver...") - inky = Inkycal(render=True) # Initialise Inkycal - print("clearing display...") - inky.calibrate(cycles=1) # Calibrate the display - print("clear complete...") - print("finished!") - - async def run(): + """Run Inkycal nonstop. Default mode.""" # create an instance of Inkycal # If your settings.json file is not in /boot, use the full path: # inky = Inkycal('path/to/settings.json', render=True) @@ -36,5 +20,24 @@ async def run(): await inky.run() # If there were no issues, you can run Inkycal nonstop +async def dry_run(): + """Useful for checking if the settings.json file is okay, without actually touching the display""" + # create an instance of Inkycal + # If your settings.json file is not in /boot, use the full path: + # inky = Inkycal('path/to/settings.json', render=True) + inky = Inkycal(render=False) + await inky.run(run_once=True) # dry-run without rendering anything on the display + + +async def clear_display(): + """Calibrate the display if you see some ghosting""" + print("loading Inkycal and display driver...") + inky = Inkycal(render=True) # Initialise Inkycal + print("clearing display...") + inky.calibrate(cycles=1) # Calibrate the display + print("clear complete...") + print("finished!") + + if __name__ == "__main__": asyncio.run(run()) diff --git a/inkycal/display/display.py b/inkycal/display/display.py index 4eb47f3..bdb0818 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -1,6 +1,6 @@ """ Inkycal ePaper driving functions -Copyright by aceisace +Copyright by aceinnolab """ from importlib import import_module diff --git a/inkycal/main.py b/inkycal/main.py index 476f683..ed8da92 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -39,7 +39,7 @@ class Inkycal: def __init__(self, settings_path: str or None = None, render: bool = True, use_pi_sugar: bool = False): """Initialise Inkycal""" - self._release = "2.0.3" + self._release = "2.0.4" logger.info(f"Inkycal v{self._release} booting up...") diff --git a/setup.py b/setup.py index 6064cce..e120a94 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,10 @@ with open('requirements.txt') as f: required = [i.split(' ')[0] for i in required] __project__ = "inkycal" -__version__ = "2.0.3" +__version__ = "2.0.4" __description__ = "Inkycal is a python3 software for syncing icalendar events, weather and news on selected E-Paper displays" __packages__ = ["inkycal"] -__author__ = "aceisace" +__author__ = "aceinnolab" __author_email__ = "aceisace63@yahoo.com" __url__ = "https://github.com/aceinnolab/Inkycal" From 274b0d492775c0070c28186eba9aba79852e824a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Jun 2024 23:49:02 +0000 Subject: [PATCH 32/62] update docs [bot] --- docs/_static/documentation_options.js | 2 +- docs/about.html | 4 ++-- docs/dev_doc.html | 4 ++-- docs/genindex.html | 4 ++-- docs/index.html | 4 ++-- docs/inkycal.html | 4 ++-- docs/py-modindex.html | 4 ++-- docs/quickstart.html | 4 ++-- docs/search.html | 4 ++-- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 138ae34..d5f566d 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '2.0.3', + VERSION: '2.0.4', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/about.html b/docs/about.html index 2fa7e79..1adec59 100644 --- a/docs/about.html +++ b/docs/about.html @@ -4,7 +4,7 @@ - About Inkycal — inkycal 2.0.3 documentation + About Inkycal — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/dev_doc.html b/docs/dev_doc.html index 269304c..6282926 100644 --- a/docs/dev_doc.html +++ b/docs/dev_doc.html @@ -4,7 +4,7 @@ - Developer documentation — inkycal 2.0.3 documentation + Developer documentation — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/genindex.html b/docs/genindex.html index be82be7..de6517a 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -3,7 +3,7 @@ - Index — inkycal 2.0.3 documentation + Index — inkycal 2.0.4 documentation @@ -14,7 +14,7 @@ - + diff --git a/docs/index.html b/docs/index.html index 7ec0933..05ba2c8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ - Inkycal documentation — inkycal 2.0.3 documentation + Inkycal documentation — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/inkycal.html b/docs/inkycal.html index b0b38f6..16c46d8 100644 --- a/docs/inkycal.html +++ b/docs/inkycal.html @@ -4,7 +4,7 @@ - Inkycal — inkycal 2.0.3 documentation + Inkycal — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/py-modindex.html b/docs/py-modindex.html index 063dd06..9bb91e7 100644 --- a/docs/py-modindex.html +++ b/docs/py-modindex.html @@ -3,7 +3,7 @@ - Python Module Index — inkycal 2.0.3 documentation + Python Module Index — inkycal 2.0.4 documentation @@ -14,7 +14,7 @@ - + diff --git a/docs/quickstart.html b/docs/quickstart.html index d09f5ba..be927d0 100644 --- a/docs/quickstart.html +++ b/docs/quickstart.html @@ -4,7 +4,7 @@ - Quickstart — inkycal 2.0.3 documentation + Quickstart — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + diff --git a/docs/search.html b/docs/search.html index 495e5fb..5923959 100644 --- a/docs/search.html +++ b/docs/search.html @@ -3,7 +3,7 @@ - Search — inkycal 2.0.3 documentation + Search — inkycal 2.0.4 documentation @@ -15,7 +15,7 @@ - + From 9d691acfc597709d0f328e389a84ff0d3e740036 Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 27 Jun 2024 01:55:23 +0200 Subject: [PATCH 33/62] add shutdown option & fix german weather strings --- inky_run.py | 2 +- inkycal/main.py | 28 +++++++++++++++++++++++++--- inkycal/modules/inkycal_weather.py | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/inky_run.py b/inky_run.py index 711b2dc..b0b6b31 100644 --- a/inky_run.py +++ b/inky_run.py @@ -15,7 +15,7 @@ async def run(): # inky = Inkycal('path/to/settings.json', render=True) # when using experimental PiSugar support: - # inky = Inkycal(render=True, use_pi_sugar=True) + # inky = Inkycal(render=True, use_pi_sugar=True, shutdown_after_run=False) inky = Inkycal(render=True) await inky.run() # If there were no issues, you can run Inkycal nonstop diff --git a/inkycal/main.py b/inkycal/main.py index ed8da92..bf2fe86 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -37,8 +37,21 @@ 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, use_pi_sugar: bool = False): - """Initialise Inkycal""" + def __init__(self, settings_path: str or None = None, render: bool = True, use_pi_sugar: bool = False, + shutdown_after_run: bool = False) -> None: + """Initialise Inkycal + + Args: + settings_path (str): + The full path to your settings.json file. If no path was specified, will look in the /boot directory. + render (bool): + Show the image on the E-Paper display. + use_pi_sugar (bool): + Use PiSugar board (all revisions). Default is False. + shutdown_after_run (bool): + Shutdown the system after the run is complete. Will only work with PiSugar enabled. + + """ self._release = "2.0.4" logger.info(f"Inkycal v{self._release} booting up...") @@ -51,7 +64,7 @@ class Inkycal: if settings_path: logger.info(f"Custom location for settings.json file specified: {settings_path}") try: - with open(settings_path) as settings_file: + with open(settings_path, mode="r") as settings_file: self.settings = json.load(settings_file) except FileNotFoundError: @@ -141,6 +154,7 @@ class Inkycal: self.use_pi_sugar = use_pi_sugar self.battery_capacity = 100 + self.shutdown_after_run = use_pi_sugar and shutdown_after_run if self.use_pi_sugar: logger.info("PiSugar support enabled.") @@ -162,6 +176,9 @@ class Inkycal: print( f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}") + if self.shutdown_after_run: + logger.warning("Shutdown after run enabled. System will shutdown after the run is complete.") + # Give an OK message logger.info('Inkycal initialised successfully!') @@ -382,6 +399,11 @@ class Inkycal: result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127) if result: logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}") + if self.shutdown_after_run: + logger.warning("System shutdown in 5 seconds!") + time.sleep(5) + self._shutdown_system() + break else: logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}") diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 2c32b0b..f5861f4 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -518,7 +518,7 @@ class Weather(inkycal_module): stamp = fc_data[f'fc{pos}']['stamp'] # check if we're using daily forecasts if "day" in stamp: - stamp = arrow.get(fc_data[f'fc{pos}']['stamp'], "dddd").format("dddd", locale="de") + stamp = arrow.get(fc_data[f'fc{pos}']['stamp'], "dddd").format("dddd", locale=self.locale) icon = weather_icons[fc_data[f'fc{pos}']['icon']] temp = fc_data[f'fc{pos}']['temp'] From ec0833cfb8a95147c6a82dbf3e89cc2335f365c9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 26 Jun 2024 23:56:37 +0000 Subject: [PATCH 34/62] update docs [bot] --- docs/inkycal.html | 2 +- docs/searchindex.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/inkycal.html b/docs/inkycal.html index 16c46d8..405ed67 100644 --- a/docs/inkycal.html +++ b/docs/inkycal.html @@ -131,7 +131,7 @@ Copyright by aceinnolab

-class inkycal.main.Inkycal(settings_path: str = None, render: bool = True, use_pi_sugar: bool = False)
+class inkycal.main.Inkycal(settings_path: str = None, render: bool = True, use_pi_sugar: bool = False, shutdown_after_run: bool = False)

Inkycal main class

Main class of Inkycal, test and run the main Inkycal program.

diff --git a/docs/searchindex.js b/docs/searchindex.js index eb646da..9b7ecaf 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"About Inkycal": [[0, "about-inkycal"]], "Contents:": [[2, null]], "Creating settings file": [[4, "creating-settings-file"]], "Custom functions": [[3, "module-inkycal.custom.functions"]], "Developer documentation": [[1, "developer-documentation"]], "Display": [[3, "module-inkycal.display.Display"]], "Helper classes": [[3, "module-inkycal.modules.ical_parser"]], "Indices and tables": [[2, "indices-and-tables"]], "Inkycal": [[3, "module-inkycal.main"]], "Inkycal documentation": [[2, "inkycal-documentation"]], "Installing Inkycal": [[4, "installing-inkycal"]], "Quickstart": [[4, "quickstart"]]}, "docnames": ["about", "dev_doc", "index", "inkycal", "quickstart"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["about.md", "dev_doc.md", "index.rst", "inkycal.rst", "quickstart.md"], "indexentries": {"all_day() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.all_day", false]], "auto_fontsize() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.auto_fontsize", false]], "autoflip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.autoflip", false]], "calibrate() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.calibrate", false]], "clear() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.clear", false]], "clear_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.clear_events", false]], "countdown() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.countdown", false]], "draw_border() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.draw_border", false]], "dry_run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.dry_run", false]], "flip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.flip", false]], "get_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_events", false]], "get_fonts() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_fonts", false]], "get_system_tz() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_system_tz", false]], "get_system_tz() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_system_tz", false]], "icalendar (class in inkycal.modules.ical_parser)": [[3, "inkycal.modules.ical_parser.iCalendar", false]], "image_to_palette() (in module inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.image_to_palette", false]], "inkycal (class in inkycal.main)": [[3, "inkycal.main.Inkycal", false]], "inkycal.custom.functions": [[3, "module-inkycal.custom.functions", false]], "inkycal.display.display": [[3, "module-inkycal.display.Display", false]], "inkycal.main": [[3, "module-inkycal.main", false]], "inkycal.modules.ical_parser": [[3, "module-inkycal.modules.ical_parser", false]], "inkycal.modules.inky_image": [[3, "module-inkycal.modules.inky_image", false]], "inkyimage (class in inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.Inkyimage", false]], "internet_available() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.internet_available", false]], "load() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.load", false]], "load_from_file() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_from_file", false]], "load_url() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_url", false]], "merge() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.merge", false]], "module": [[3, "module-inkycal.custom.functions", false], [3, "module-inkycal.display.Display", false], [3, "module-inkycal.main", false], [3, "module-inkycal.modules.ical_parser", false], [3, "module-inkycal.modules.inky_image", false]], "preview() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.preview", false]], "process_module() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.process_module", false]], "remove_alpha() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.remove_alpha", false]], "resize() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.resize", false]], "run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.run", false]], "show_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.show_events", false]], "sort() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.sort", false]], "text_wrap() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.text_wrap", false]], "write() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.write", false]]}, "objects": {"inkycal": [[3, 0, 0, "-", "main"]], "inkycal.custom": [[3, 0, 0, "-", "functions"]], "inkycal.custom.functions": [[3, 1, 1, "", "auto_fontsize"], [3, 1, 1, "", "draw_border"], [3, 1, 1, "", "get_fonts"], [3, 1, 1, "", "get_system_tz"], [3, 1, 1, "", "internet_available"], [3, 1, 1, "", "text_wrap"], [3, 1, 1, "", "write"]], "inkycal.display": [[3, 0, 0, "-", "Display"]], "inkycal.main": [[3, 2, 1, "", "Inkycal"]], "inkycal.main.Inkycal": [[3, 3, 1, "", "calibrate"], [3, 3, 1, "", "countdown"], [3, 3, 1, "", "dry_run"], [3, 3, 1, "", "process_module"], [3, 3, 1, "", "run"]], "inkycal.modules": [[3, 0, 0, "-", "ical_parser"], [3, 0, 0, "-", "inky_image"]], "inkycal.modules.ical_parser": [[3, 2, 1, "", "iCalendar"]], "inkycal.modules.ical_parser.iCalendar": [[3, 3, 1, "", "all_day"], [3, 3, 1, "", "clear_events"], [3, 3, 1, "", "get_events"], [3, 3, 1, "", "get_system_tz"], [3, 3, 1, "", "load_from_file"], [3, 3, 1, "", "load_url"], [3, 3, 1, "", "show_events"], [3, 3, 1, "", "sort"]], "inkycal.modules.inky_image": [[3, 2, 1, "", "Inkyimage"], [3, 1, 1, "", "image_to_palette"]], "inkycal.modules.inky_image.Inkyimage": [[3, 3, 1, "", "autoflip"], [3, 3, 1, "", "clear"], [3, 3, 1, "", "flip"], [3, 3, 1, "", "load"], [3, 3, 1, "", "merge"], [3, 3, 1, "", "preview"], [3, 3, 1, "", "remove_alpha"], [3, 3, 1, "", "resize"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"": [0, 3], "0": 3, "1": 3, "10": 3, "100": 3, "11": 3, "16": 3, "16grai": 3, "180": 3, "2": 3, "20": 3, "270": 3, "3": 3, "32": 3, "360": 3, "5": 3, "7": 3, "80": 3, "9": 3, "90": 3, "A": 3, "If": 3, "In": 3, "It": 0, "No": 0, "OR": 3, "The": [0, 3], "Then": 3, "To": 3, "_": 0, "about": 2, "access": 3, "accur": 3, "aceinnolab": [3, 4], "aceisac": 0, "actual": 3, "ad": 0, "add": 3, "adjust": 3, "after": 3, "agenda": 0, "align": 3, "aliv": 0, "all": [0, 3], "all_dai": 3, "allow": 3, "alpha": 3, "also": 0, "amount": 0, "an": 3, "angl": 3, "anti": 3, "anyth": 0, "arg": 3, "arrow": 3, "assembl": 3, "async": 3, "atom": 0, "attempt": 3, "attribut": 3, "auto_fonts": [2, 3], "autofit": 3, "autoflip": 3, "automat": 3, "avail": 3, "awar": 3, "band": 3, "base": 3, "befor": 3, "begin": 3, "behind": 0, "below": 3, "black": 3, "blend": 0, "bool": 3, "boot": 3, "border": 3, "box": 3, "box_siz": 3, "built": 0, "bw": 3, "bwr": 3, "bwy": 3, "calcul": 3, "calendar": 0, "calibr": 3, "can": [0, 3], "care": [0, 3], "case": 0, "caus": 3, "cd": 4, "center": 3, "chang": 3, "check": 3, "choos": 3, "chunk": 3, "class": 2, "clear": 3, "clear_ev": 3, "clockwis": 3, "clone": 4, "co": 3, "coffe": 0, "colour": 3, "com": [3, 4], "come": 4, "commerci": 0, "commonli": 3, "commun": 0, "compat": 0, "config": 3, "connect": 3, "contain": 3, "coordin": 3, "copi": 4, "copyright": 3, "corner": 3, "correct": 3, "correctli": 3, "could": 3, "countdown": 3, "cours": 0, "creat": [0, 1, 2, 3], "current": 3, "custom": 2, "cycl": 3, "dai": 3, "dashboard": 0, "date": 3, "dd": 3, "decim": 3, "default": 3, "defin": 3, "desir": 3, "desktop": 3, "detail": 0, "develop": [0, 2], "dictionari": 3, "directli": 4, "discord": 0, "displai": [0, 2], "dither": 3, "do": 3, "doesn": [0, 3], "don": 0, "donat": 0, "download": [3, 4], "draw": 3, "draw_bord": [2, 3], "drawn": 3, "driver": 3, "dry_run": 3, "e": [0, 3, 4], "each": 3, "eas": 3, "edit": 0, "effort": 0, "els": 3, "en": 3, "end": 3, "environ": 0, "epap": 3, "epaper_model": 3, "establish": 3, "etc": 0, "even": 0, "event": [0, 3], "exampl": 3, "except": 3, "exit": 3, "extract": 3, "face": 0, "fals": 3, "feed": 0, "fetch": 0, "few": 0, "file": [0, 2, 3], "filenotfounderror": 3, "filepath": 3, "fill": 3, "fill_height": 3, "fill_width": 3, "first": 3, "fit": 3, "flag": 3, "flip": 3, "fmt": 3, "folder": [3, 4], "follow": 3, "font": 3, "fontfil": 3, "fontnam": 3, "fontsiz": 3, "forecast": 0, "form": 0, "format": 3, "found": 3, "free": 0, "friendli": 0, "from": [0, 3], "full": [0, 3], "fulli": 0, "function": 2, "g": 3, "gener": [3, 4], "get": [0, 3], "get_ev": 3, "get_font": [2, 3], "get_system_tz": [2, 3], "git": 4, "github": 4, "given": 3, "go": 4, "googl": [0, 3], "gpicview": 3, "grai": 3, "greater": 3, "ha": [0, 3], "handl": 3, "have": [0, 3], "height": 3, "height_shrink_percentag": 3, "help": 0, "helper": 2, "hh": 3, "home": 3, "horizont": 3, "hour": 0, "htpp": 3, "http": [3, 4], "i": [0, 1, 3], "ical_pars": 3, "icalendar": [0, 2, 3], "idea": 0, "im_black": 3, "im_colour": 3, "imag": 3, "image1": 3, "image2": 3, "image_to_palett": [2, 3], "imagefont": 3, "imga": 3, "import": 3, "improv": 3, "increas": 3, "indefinit": 3, "index": 2, "individu": 3, "infin": 3, "info": 3, "inform": 0, "initi": 3, "inky_imag": 3, "inkyimag": [2, 3], "input": 3, "instal": 2, "instanc": 3, "instead": 3, "int": 3, "integ": 3, "internet": 3, "internet_avail": [2, 3], "interv": 3, "interval_min": 3, "invest": 0, "io": 3, "issu": 3, "iter": 3, "its": 0, "joke": 0, "json": 3, "keep": 0, "kwarg": 3, "larg": 0, "latest": [0, 3], "layout": 3, "left": 3, "lib": 3, "line": 3, "list": 3, "liter": 3, "load": 3, "load_from_fil": 3, "load_url": 3, "local": 3, "logo": 3, "long": 3, "look": [0, 3], "loop": 3, "made": 3, "mai": 0, "main": [0, 3], "mainli": [0, 1], "map": 3, "max_height": 3, "max_width": 3, "maximum": 3, "mean": 0, "merg": 3, "minut": 3, "miss": 0, "mm": 3, "mmm": 3, "mode": 3, "model": 3, "modifi": 3, "modul": [0, 1, 2, 3], "modular": 0, "monthli": 0, "more": [0, 3, 4], "moudul": 3, "much": 3, "multipl": 3, "name": 3, "navig": 4, "need": 0, "network": 3, "new": [0, 3], "next": [0, 3], "nice_p": 3, "non": [0, 3], "none": 3, "nonstop": 3, "noob": 0, "noth": 0, "now": 3, "number": 3, "object": 3, "onc": 3, "one": 3, "ones": 3, "onli": 3, "open": 0, "oper": 3, "optim": 3, "option": 3, "order": 3, "ordin": 3, "organis": 0, "oserror": 3, "other": [0, 3], "output": 3, "own": 0, "packag": 3, "page": 2, "palett": 3, "paper": [0, 3], "paramet": 3, "pars": 3, "part": 3, "parti": [0, 1], "password": 3, "past": 3, "path": 3, "path1": 3, "path2": 3, "percentag": 3, "phone": 0, "pi": [0, 3, 4], "pil": 3, "pinch": 0, "pip3": 4, "pixel": 3, "plain": 3, "pleas": [0, 4], "png": 3, "point": 3, "posit": 3, "possibl": 3, "present": 3, "preview": 3, "previous": 3, "print": 3, "process": 3, "process_modul": 3, "program": 3, "project": [0, 3], "protect": 3, "provid": 0, "py": 3, "python3": [0, 3], "quickstart": 2, "radiu": 3, "rais": 3, "rapsbian": 3, "raspberri": [0, 4], "raw": 3, "re": 0, "reach": 3, "readabl": 3, "readthedoc": 3, "rectangl": 3, "red": 3, "reduc": 3, "refresh": 3, "remain": 3, "remov": 3, "remove_alpha": 3, "render": 3, "replac": 3, "repo": 4, "repres": 3, "requir": 3, "resiz": 3, "return": 3, "rgba": 3, "right": 3, "rotat": 3, "round": 3, "rss": 0, "run": [0, 3], "run_onc": 3, "runner": 3, "sampl": 3, "save": 3, "scale": 3, "schedul": 3, "search": [2, 3], "second": 3, "see": 3, "select": [0, 3], "set": [0, 2, 3], "settings_path": 3, "sever": 0, "shade": 3, "share": 0, "should": 3, "show": [0, 3], "show_ev": 3, "shown": 3, "shrink": 3, "shrinkag": 3, "singl": 3, "site": 3, "size": 3, "sleep": 3, "smaller": 3, "smile": 0, "softwar": 0, "solid": 3, "some": 0, "someth": [0, 3], "soon": 4, "sort": 3, "sourc": 0, "specifi": 3, "split": 3, "stai": 0, "start": 3, "static": 3, "stop": 3, "str": 3, "string": 3, "student": 0, "support": [0, 3], "sync": 0, "synchronis": 0, "system": 3, "sytax": 3, "t": [0, 3], "take": [0, 3], "test": 3, "text": 3, "text_wrap": [2, 3], "than": 3, "thank": 0, "them": [0, 3], "thi": [0, 1, 3], "thick": 3, "third": [0, 1], "time": [0, 3], "timelin": 3, "timeline_end": 3, "timeline_start": 3, "timeout": 3, "timezon": 3, "token": 3, "too": 0, "top": 3, "transpar": 3, "tri": 3, "true": 3, "truetyp": 3, "tupl": 3, "two": 3, "type": 3, "typeerror": 3, "tz": 3, "u": 0, "ui": [0, 4], "univers": 0, "until": 3, "up": 0, "updat": 3, "url": 3, "url1": 3, "url2": 3, "us": 3, "use_pi_sugar": 3, "user": 0, "usernam": 3, "usual": 3, "utc": 3, "valid": 3, "valu": 3, "valueerror": 3, "venv": 3, "veri": 3, "vertic": 3, "via": [0, 4], "wa": [0, 3], "wai": 3, "we": 0, "weather": 0, "web": [0, 4], "week": 0, "welcom": 0, "well": 0, "what": 0, "when": 3, "where": 3, "which": [0, 3], "white": 3, "who": 1, "width": 3, "width_shrink_percentag": 3, "wish": 1, "without": [0, 3], "work": [0, 3], "write": [0, 2, 3], "written": 3, "x": 3, "xy": 3, "y": 3, "yai": 0, "yellow": 3, "you": 0, "your": [0, 3, 4], "yy": 3, "zero": 0}, "titles": ["About Inkycal", "Developer documentation", "Inkycal documentation", "Inkycal", "Quickstart"], "titleterms": {"about": 0, "class": 3, "content": 2, "creat": 4, "custom": 3, "develop": 1, "displai": 3, "document": [1, 2], "file": 4, "function": 3, "helper": 3, "indic": 2, "inkyc": [0, 2, 3, 4], "instal": 4, "quickstart": 4, "set": 4, "tabl": 2}}) \ No newline at end of file +Search.setIndex({"alltitles": {"About Inkycal": [[0, "about-inkycal"]], "Contents:": [[2, null]], "Creating settings file": [[4, "creating-settings-file"]], "Custom functions": [[3, "module-inkycal.custom.functions"]], "Developer documentation": [[1, "developer-documentation"]], "Display": [[3, "module-inkycal.display.Display"]], "Helper classes": [[3, "module-inkycal.modules.ical_parser"]], "Indices and tables": [[2, "indices-and-tables"]], "Inkycal": [[3, "module-inkycal.main"]], "Inkycal documentation": [[2, "inkycal-documentation"]], "Installing Inkycal": [[4, "installing-inkycal"]], "Quickstart": [[4, "quickstart"]]}, "docnames": ["about", "dev_doc", "index", "inkycal", "quickstart"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["about.md", "dev_doc.md", "index.rst", "inkycal.rst", "quickstart.md"], "indexentries": {"all_day() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.all_day", false]], "auto_fontsize() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.auto_fontsize", false]], "autoflip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.autoflip", false]], "calibrate() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.calibrate", false]], "clear() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.clear", false]], "clear_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.clear_events", false]], "countdown() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.countdown", false]], "draw_border() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.draw_border", false]], "dry_run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.dry_run", false]], "flip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.flip", false]], "get_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_events", false]], "get_fonts() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_fonts", false]], "get_system_tz() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_system_tz", false]], "get_system_tz() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_system_tz", false]], "icalendar (class in inkycal.modules.ical_parser)": [[3, "inkycal.modules.ical_parser.iCalendar", false]], "image_to_palette() (in module inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.image_to_palette", false]], "inkycal (class in inkycal.main)": [[3, "inkycal.main.Inkycal", false]], "inkycal.custom.functions": [[3, "module-inkycal.custom.functions", false]], "inkycal.display.display": [[3, "module-inkycal.display.Display", false]], "inkycal.main": [[3, "module-inkycal.main", false]], "inkycal.modules.ical_parser": [[3, "module-inkycal.modules.ical_parser", false]], "inkycal.modules.inky_image": [[3, "module-inkycal.modules.inky_image", false]], "inkyimage (class in inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.Inkyimage", false]], "internet_available() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.internet_available", false]], "load() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.load", false]], "load_from_file() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_from_file", false]], "load_url() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_url", false]], "merge() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.merge", false]], "module": [[3, "module-inkycal.custom.functions", false], [3, "module-inkycal.display.Display", false], [3, "module-inkycal.main", false], [3, "module-inkycal.modules.ical_parser", false], [3, "module-inkycal.modules.inky_image", false]], "preview() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.preview", false]], "process_module() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.process_module", false]], "remove_alpha() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.remove_alpha", false]], "resize() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.resize", false]], "run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.run", false]], "show_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.show_events", false]], "sort() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.sort", false]], "text_wrap() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.text_wrap", false]], "write() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.write", false]]}, "objects": {"inkycal": [[3, 0, 0, "-", "main"]], "inkycal.custom": [[3, 0, 0, "-", "functions"]], "inkycal.custom.functions": [[3, 1, 1, "", "auto_fontsize"], [3, 1, 1, "", "draw_border"], [3, 1, 1, "", "get_fonts"], [3, 1, 1, "", "get_system_tz"], [3, 1, 1, "", "internet_available"], [3, 1, 1, "", "text_wrap"], [3, 1, 1, "", "write"]], "inkycal.display": [[3, 0, 0, "-", "Display"]], "inkycal.main": [[3, 2, 1, "", "Inkycal"]], "inkycal.main.Inkycal": [[3, 3, 1, "", "calibrate"], [3, 3, 1, "", "countdown"], [3, 3, 1, "", "dry_run"], [3, 3, 1, "", "process_module"], [3, 3, 1, "", "run"]], "inkycal.modules": [[3, 0, 0, "-", "ical_parser"], [3, 0, 0, "-", "inky_image"]], "inkycal.modules.ical_parser": [[3, 2, 1, "", "iCalendar"]], "inkycal.modules.ical_parser.iCalendar": [[3, 3, 1, "", "all_day"], [3, 3, 1, "", "clear_events"], [3, 3, 1, "", "get_events"], [3, 3, 1, "", "get_system_tz"], [3, 3, 1, "", "load_from_file"], [3, 3, 1, "", "load_url"], [3, 3, 1, "", "show_events"], [3, 3, 1, "", "sort"]], "inkycal.modules.inky_image": [[3, 2, 1, "", "Inkyimage"], [3, 1, 1, "", "image_to_palette"]], "inkycal.modules.inky_image.Inkyimage": [[3, 3, 1, "", "autoflip"], [3, 3, 1, "", "clear"], [3, 3, 1, "", "flip"], [3, 3, 1, "", "load"], [3, 3, 1, "", "merge"], [3, 3, 1, "", "preview"], [3, 3, 1, "", "remove_alpha"], [3, 3, 1, "", "resize"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"": [0, 3], "0": 3, "1": 3, "10": 3, "100": 3, "11": 3, "16": 3, "16grai": 3, "180": 3, "2": 3, "20": 3, "270": 3, "3": 3, "32": 3, "360": 3, "5": 3, "7": 3, "80": 3, "9": 3, "90": 3, "A": 3, "If": 3, "In": 3, "It": 0, "No": 0, "OR": 3, "The": [0, 3], "Then": 3, "To": 3, "_": 0, "about": 2, "access": 3, "accur": 3, "aceinnolab": [3, 4], "aceisac": 0, "actual": 3, "ad": 0, "add": 3, "adjust": 3, "after": 3, "agenda": 0, "align": 3, "aliv": 0, "all": [0, 3], "all_dai": 3, "allow": 3, "alpha": 3, "also": 0, "amount": 0, "an": 3, "angl": 3, "anti": 3, "anyth": 0, "arg": 3, "arrow": 3, "assembl": 3, "async": 3, "atom": 0, "attempt": 3, "attribut": 3, "auto_fonts": [2, 3], "autofit": 3, "autoflip": 3, "automat": 3, "avail": 3, "awar": 3, "band": 3, "base": 3, "befor": 3, "begin": 3, "behind": 0, "below": 3, "black": 3, "blend": 0, "bool": 3, "boot": 3, "border": 3, "box": 3, "box_siz": 3, "built": 0, "bw": 3, "bwr": 3, "bwy": 3, "calcul": 3, "calendar": 0, "calibr": 3, "can": [0, 3], "care": [0, 3], "case": 0, "caus": 3, "cd": 4, "center": 3, "chang": 3, "check": 3, "choos": 3, "chunk": 3, "class": 2, "clear": 3, "clear_ev": 3, "clockwis": 3, "clone": 4, "co": 3, "coffe": 0, "colour": 3, "com": [3, 4], "come": 4, "commerci": 0, "commonli": 3, "commun": 0, "compat": 0, "config": 3, "connect": 3, "contain": 3, "coordin": 3, "copi": 4, "copyright": 3, "corner": 3, "correct": 3, "correctli": 3, "could": 3, "countdown": 3, "cours": 0, "creat": [0, 1, 2, 3], "current": 3, "custom": 2, "cycl": 3, "dai": 3, "dashboard": 0, "date": 3, "dd": 3, "decim": 3, "default": 3, "defin": 3, "desir": 3, "desktop": 3, "detail": 0, "develop": [0, 2], "dictionari": 3, "directli": 4, "discord": 0, "displai": [0, 2], "dither": 3, "do": 3, "doesn": [0, 3], "don": 0, "donat": 0, "download": [3, 4], "draw": 3, "draw_bord": [2, 3], "drawn": 3, "driver": 3, "dry_run": 3, "e": [0, 3, 4], "each": 3, "eas": 3, "edit": 0, "effort": 0, "els": 3, "en": 3, "end": 3, "environ": 0, "epap": 3, "epaper_model": 3, "establish": 3, "etc": 0, "even": 0, "event": [0, 3], "exampl": 3, "except": 3, "exit": 3, "extract": 3, "face": 0, "fals": 3, "feed": 0, "fetch": 0, "few": 0, "file": [0, 2, 3], "filenotfounderror": 3, "filepath": 3, "fill": 3, "fill_height": 3, "fill_width": 3, "first": 3, "fit": 3, "flag": 3, "flip": 3, "fmt": 3, "folder": [3, 4], "follow": 3, "font": 3, "fontfil": 3, "fontnam": 3, "fontsiz": 3, "forecast": 0, "form": 0, "format": 3, "found": 3, "free": 0, "friendli": 0, "from": [0, 3], "full": [0, 3], "fulli": 0, "function": 2, "g": 3, "gener": [3, 4], "get": [0, 3], "get_ev": 3, "get_font": [2, 3], "get_system_tz": [2, 3], "git": 4, "github": 4, "given": 3, "go": 4, "googl": [0, 3], "gpicview": 3, "grai": 3, "greater": 3, "ha": [0, 3], "handl": 3, "have": [0, 3], "height": 3, "height_shrink_percentag": 3, "help": 0, "helper": 2, "hh": 3, "home": 3, "horizont": 3, "hour": 0, "htpp": 3, "http": [3, 4], "i": [0, 1, 3], "ical_pars": 3, "icalendar": [0, 2, 3], "idea": 0, "im_black": 3, "im_colour": 3, "imag": 3, "image1": 3, "image2": 3, "image_to_palett": [2, 3], "imagefont": 3, "imga": 3, "import": 3, "improv": 3, "increas": 3, "indefinit": 3, "index": 2, "individu": 3, "infin": 3, "info": 3, "inform": 0, "initi": 3, "inky_imag": 3, "inkyimag": [2, 3], "input": 3, "instal": 2, "instanc": 3, "instead": 3, "int": 3, "integ": 3, "internet": 3, "internet_avail": [2, 3], "interv": 3, "interval_min": 3, "invest": 0, "io": 3, "issu": 3, "iter": 3, "its": 0, "joke": 0, "json": 3, "keep": 0, "kwarg": 3, "larg": 0, "latest": [0, 3], "layout": 3, "left": 3, "lib": 3, "line": 3, "list": 3, "liter": 3, "load": 3, "load_from_fil": 3, "load_url": 3, "local": 3, "logo": 3, "long": 3, "look": [0, 3], "loop": 3, "made": 3, "mai": 0, "main": [0, 3], "mainli": [0, 1], "map": 3, "max_height": 3, "max_width": 3, "maximum": 3, "mean": 0, "merg": 3, "minut": 3, "miss": 0, "mm": 3, "mmm": 3, "mode": 3, "model": 3, "modifi": 3, "modul": [0, 1, 2, 3], "modular": 0, "monthli": 0, "more": [0, 3, 4], "moudul": 3, "much": 3, "multipl": 3, "name": 3, "navig": 4, "need": 0, "network": 3, "new": [0, 3], "next": [0, 3], "nice_p": 3, "non": [0, 3], "none": 3, "nonstop": 3, "noob": 0, "noth": 0, "now": 3, "number": 3, "object": 3, "onc": 3, "one": 3, "ones": 3, "onli": 3, "open": 0, "oper": 3, "optim": 3, "option": 3, "order": 3, "ordin": 3, "organis": 0, "oserror": 3, "other": [0, 3], "output": 3, "own": 0, "packag": 3, "page": 2, "palett": 3, "paper": [0, 3], "paramet": 3, "pars": 3, "part": 3, "parti": [0, 1], "password": 3, "past": 3, "path": 3, "path1": 3, "path2": 3, "percentag": 3, "phone": 0, "pi": [0, 3, 4], "pil": 3, "pinch": 0, "pip3": 4, "pixel": 3, "plain": 3, "pleas": [0, 4], "png": 3, "point": 3, "posit": 3, "possibl": 3, "present": 3, "preview": 3, "previous": 3, "print": 3, "process": 3, "process_modul": 3, "program": 3, "project": [0, 3], "protect": 3, "provid": 0, "py": 3, "python3": [0, 3], "quickstart": 2, "radiu": 3, "rais": 3, "rapsbian": 3, "raspberri": [0, 4], "raw": 3, "re": 0, "reach": 3, "readabl": 3, "readthedoc": 3, "rectangl": 3, "red": 3, "reduc": 3, "refresh": 3, "remain": 3, "remov": 3, "remove_alpha": 3, "render": 3, "replac": 3, "repo": 4, "repres": 3, "requir": 3, "resiz": 3, "return": 3, "rgba": 3, "right": 3, "rotat": 3, "round": 3, "rss": 0, "run": [0, 3], "run_onc": 3, "runner": 3, "sampl": 3, "save": 3, "scale": 3, "schedul": 3, "search": [2, 3], "second": 3, "see": 3, "select": [0, 3], "set": [0, 2, 3], "settings_path": 3, "sever": 0, "shade": 3, "share": 0, "should": 3, "show": [0, 3], "show_ev": 3, "shown": 3, "shrink": 3, "shrinkag": 3, "shutdown_after_run": 3, "singl": 3, "site": 3, "size": 3, "sleep": 3, "smaller": 3, "smile": 0, "softwar": 0, "solid": 3, "some": 0, "someth": [0, 3], "soon": 4, "sort": 3, "sourc": 0, "specifi": 3, "split": 3, "stai": 0, "start": 3, "static": 3, "stop": 3, "str": 3, "string": 3, "student": 0, "support": [0, 3], "sync": 0, "synchronis": 0, "system": 3, "sytax": 3, "t": [0, 3], "take": [0, 3], "test": 3, "text": 3, "text_wrap": [2, 3], "than": 3, "thank": 0, "them": [0, 3], "thi": [0, 1, 3], "thick": 3, "third": [0, 1], "time": [0, 3], "timelin": 3, "timeline_end": 3, "timeline_start": 3, "timeout": 3, "timezon": 3, "token": 3, "too": 0, "top": 3, "transpar": 3, "tri": 3, "true": 3, "truetyp": 3, "tupl": 3, "two": 3, "type": 3, "typeerror": 3, "tz": 3, "u": 0, "ui": [0, 4], "univers": 0, "until": 3, "up": 0, "updat": 3, "url": 3, "url1": 3, "url2": 3, "us": 3, "use_pi_sugar": 3, "user": 0, "usernam": 3, "usual": 3, "utc": 3, "valid": 3, "valu": 3, "valueerror": 3, "venv": 3, "veri": 3, "vertic": 3, "via": [0, 4], "wa": [0, 3], "wai": 3, "we": 0, "weather": 0, "web": [0, 4], "week": 0, "welcom": 0, "well": 0, "what": 0, "when": 3, "where": 3, "which": [0, 3], "white": 3, "who": 1, "width": 3, "width_shrink_percentag": 3, "wish": 1, "without": [0, 3], "work": [0, 3], "write": [0, 2, 3], "written": 3, "x": 3, "xy": 3, "y": 3, "yai": 0, "yellow": 3, "you": 0, "your": [0, 3, 4], "yy": 3, "zero": 0}, "titles": ["About Inkycal", "Developer documentation", "Inkycal documentation", "Inkycal", "Quickstart"], "titleterms": {"about": 0, "class": 3, "content": 2, "creat": 4, "custom": 3, "develop": 1, "displai": 3, "document": [1, 2], "file": 4, "function": 3, "helper": 3, "indic": 2, "inkyc": [0, 2, 3, 4], "instal": 4, "quickstart": 4, "set": 4, "tabl": 2}}) \ No newline at end of file From 7c4b89cb5ffed89a61d91b30140fb26916105355 Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 1 Jul 2024 18:31:07 +0200 Subject: [PATCH 35/62] bump "yanked" version of requests --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 75ea61b..3d18e25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ python-dotenv==1.0.1 pytz==2024.1 PyYAML==6.0.1 recurring-ical-events==2.1.2 -requests==2.32.0 +requests==2.32.3 sgmllib3k==1.0.0 six==1.16.0 soupsieve==2.5 From de45cb16f5bf2e20bdcec4dc00917146fbbae2a7 Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 1 Jul 2024 18:53:28 +0200 Subject: [PATCH 36/62] remove non-found dep --- .github/workflows/test-on-rpi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-on-rpi.yml b/.github/workflows/test-on-rpi.yml index 42a1c12..5e3f014 100644 --- a/.github/workflows/test-on-rpi.yml +++ b/.github/workflows/test-on-rpi.yml @@ -42,7 +42,7 @@ jobs: whoami cd /home/inky sudo apt-get update -y - sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-base libopenblas-dev -y + sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev -y echo $PWD && ls git clone https://github.com/aceinnolab/Inkycal cd Inkycal From 7e28f60bb91851dd25e3a394073b08c25c17527a Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 01:10:31 +0200 Subject: [PATCH 37/62] added note about 12.48" Inkycal fixed up some instructions --- README.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 42fde12..59633eb 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ Watch the one-minute video on getting started with Inkycal: ## Hardware guide Before you can start, please ensure you have one of the supported displays and of the supported Raspberry -Pi: `|4|3A|3B|3B+|2B|ZeroW|ZeroWH|Zero2W|`. We personally recommend the Raspberry Pi Zero W as this is relatively cheaper, uses +Pi: `|4|3A|3B|3B+|2B|ZeroW|ZeroWH|Zero2W|`. We personally recommend the Raspberry Pi Zero W as this is relatively +cheaper, uses less power and is perfect to fit in a small photo frame once you have assembled everything. **Serial** displays are usually cheaper, but slower. Their main advantage is ease of use, like being able to communicate @@ -74,24 +75,26 @@ grayscale levels, which does not compare to the 256 grayscales of LCDs, but far links below may or may not contain the required driver board. Please ensure you get the correct driver board for the display!** -| type | vendor | Where to buy | -|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. | -| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display | -| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. | -| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar | -| `[serial]` 7.5" (640x384px) -> v1 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 640x384` on amazon or similar | -| `[serial]` 7.5" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` on amazon or similar | -| `[serial]` 7.5" (880x528px) -> v3 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x528` on amazon or similar | -| `[serial]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar | -| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" E-Paper 400x300` on amazon or similar | | -| `[parallel]` 10.3" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 10.3" E-Paper 1872×1404` on amazon or similar | -| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` on amazon or similar | -| `[parallel]` 7.8" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 7.8" E-Paper 1872×1404` on amazon or similar | -| Raspberry Pi Zero W | Raspberry Pi |  Search for `Raspberry Pi Zero W` on amazon or similar | -| MicroSD card | Sandisk |  Search for `MicroSD card 8GB` on amazon or similar | +| type | vendor | Where to buy | +|---------------------------------------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 12.48" Inkycal (plug-and-play) | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-1248-build/) Pre-configured version of Inkycal with matte black aluminium designer frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 12.48" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. | +| 7.5" Inkycal (plug-and-play) | Aceinnolab (author) |  [Buy on Tindie](https://www.tindie.com/products/aceisace4444/inkycal-build-v1/) Pre-configured version of Inkycal with custom frame and a web-ui. You do not need to buy anything extra. Includes Raspberry Pi Zero W, 7.5" e-paper, microSD card, driver board, custom packaging and 1m of cable. Comes pre-assembled for plug-and-play. | +| Inkycal frame (kit -> requires wires, 7.5" Display and Zero W with microSD card | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/inkycal-frame-custom-driver-board-only/) Ultraslim frame with custom-made front and backcover inkl. ultraslim driver board). You will need a Raspberry Pi, microSD card and a 7.5" e-paper display | +| Driver board | Aceinnolab (author) | [Buy on Tindie](https://www.tindie.com/products/aceinnolab/universal-e-paper-driver-board-for-24-pin-spi/) Ultraslim, 24-pin SPI driver board for many serial e-paper displays. | +| `[serial]` 12.48" (1304×984px) display | waveshare / gooddisplay |  Search for `Waveshare 12.48" E-Paper 1304×984` on amazon or similar | +| `[serial]` 7.5" (640x384px) -> v1 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 640x384` on amazon or similar | +| `[serial]` 7.5" (800x480px) -> v2 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x480` on amazon or similar | +| `[serial]` 7.5" (880x528px) -> v3 display (2/3-colour) | waveshare / gooddisplay | Search for `Waveshare 7.5" E-Paper 800x528` on amazon or similar | +| `[serial]` 5.83" (400x300px) display | waveshare / gooddisplay | Search for `Waveshare 5.83" E-Paper 400x300` on amazon or similar | +| `[serial]` 4.2" (400x300px)display | waveshare / gooddisplay | Search for `Waveshare 4.2" E-Paper 400x300` on amazon or similar | | +| `[parallel]` 10.3" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 10.3" E-Paper 1872×1404` on amazon or similar | +| `[parallel]` 9.7" (1200×825px) display | waveshare / gooddisplay | Search for `Waveshare 9.7" E-Paper 1200×825` on amazon or similar | +| `[parallel]` 7.8" (1872×1404px) display | waveshare / gooddisplay |  Search for `Waveshare 7.8" E-Paper 1872×1404` on amazon or similar | +| Raspberry Pi Zero W | Raspberry Pi |  Search for `Raspberry Pi Zero W` on amazon or similar | +| MicroSD card | Sandisk |  Search for `MicroSD card 8GB` on amazon or similar | ## Configuring the Raspberry Pi + Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Use the following settings: @@ -169,7 +172,7 @@ Run the following steps to install Inkycal. Do **not** use sudo for this, except ```bash # Raspberry Pi specific section start -sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-base libopenblas-dev +sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python-dev-is-python3 scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev git clone https://github.com/WiringPi/WiringPi cd WiringPi ./build From 1c693d12765baeda8d5c6da035ad16a9352cf216 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 01:19:40 +0200 Subject: [PATCH 38/62] fix non-found package --- .github/workflows/update-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 32e34bc..8acb810 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -40,7 +40,7 @@ jobs: # sudo apt-get dist-upgrade -y sudo apt-get install -y python3-pip - sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-base libopenblas-dev libxml2-dev libxslt-dev python-dev-is-python3 -y + sudo apt-get install git zlib1g libjpeg-dev libatlas-base-dev rustc libopenjp2-7 python3-dev scons libssl-dev python3-venv python3-pip git libfreetype6-dev wkhtmltopdf libopenblas-dev libxml2-dev libxslt-dev python-dev-is-python3 -y # #334 & #335 git clone https://github.com/WiringPi/WiringPi cd WiringPi From 6afb83158dfe48f97c3dcc67dde824b81421dfc4 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 01:35:24 +0200 Subject: [PATCH 39/62] updated --- .github/workflows/update-os.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 8acb810..fec198d 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -85,7 +85,8 @@ jobs: sudo chown -R inky:inky /home/inky/Inkycal # make all users require a password for sudo commands (improves security) - echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password + # temporarily disabled to allow pisugar support + # echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password # allow some time to unmount sleep 10 From 02ff5329679c3c4b2eb385a4307145c9331ef2ea Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 01:54:27 +0200 Subject: [PATCH 40/62] Update --- .github/workflows/update-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index fec198d..cb9066f 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -25,7 +25,7 @@ jobs: with: # Set the base_image to the desired Raspberry Pi OS version base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz - image_additional_mb: 3072 # enlarge free space to 3 GB + image_additional_mb: 3584 # enlarge free space to 3.5 GB optimize_image: true commands: | cd /home From d4c9e11011f1fa7713669c05669ede209fd39fbf Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 01:57:23 +0200 Subject: [PATCH 41/62] added latest modules added PiSugar info --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59633eb..e7d54b1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ready-to-flash version of Inkycal called InkycalOS-Lite with everything pre-inst via [GitHub Sponsors](https://github.com/sponsors/aceisace). This helps keep up maintenance costs, implement new features and fixing bugs. Please choose the one-time sponsor option and select the one with the plug-and-play version of Inkycal. Then, send your email-address to which InkycalOS-Lite should be sent. -Alternatively, you can also use the paypal.me link and send the same amount as Github sponsors to get access to +Alternatively, you can also use the PayPal.me link and send the same amount as GitHub sponsors to get access to InkycalOS-Lite! ## Main features @@ -42,10 +42,13 @@ following built-in modules are supported: * Image - Display an Image from URL or local file path. * Slideshow - Cycle through images in a given folder and show them on the E-Paper. * Feeds - Synchronise RSS/ATOM feeds from your favorite providers. -* Stocks - Display stocks using Tickers from Yahoo! Finance. +* Stocks - Display stocks using Tickers from Yahoo! Finance. Special thanks to @worstface * Weather - Show current weather, daily or hourly weather forecasts from openweathermap. * Todoist - Synchronise with Todoist app or website to show todos. * iCanHazDad - Display a random joke from [iCanHazDad.com](iCanhazdad.com). +* Webshot - Display a website as an image. Special thanks to @worstface +* Tindie - Show the latest orders from your Tindie store. +* XKCD - Show XKCD comics. Special thanks to @worstface ## Quickstart @@ -166,6 +169,12 @@ top of the repo to get access to Inkycal-OS-Lite. Alternatively, you can also us amount as GitHub sponsors to get access to InkycalOS-Lite! This will help keep this project growing and cover the ongoing expenses too! Win-win for everyone! 🎊 +### Bonus: PiSugar support +The PiSugar is a battery pack for the Raspberry Pi Zero W. It can be used to power the Raspberry Pi and the e-paper, allowing battery life up to several weeks. +If you have a PiSugar board, please see the wiki page on how to install the PiSugar driver and configure Inkycal to work with it: +[PiSugar support](https://github.com/aceinnolab/Inkycal/wiki/PiSugar-support) + + ### Manual installation Run the following steps to install Inkycal. Do **not** use sudo for this, except where explicitly specified. From 6eff48c28e2791604957970be9ae249cb77e6023 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 02:09:32 +0200 Subject: [PATCH 42/62] fix unmounting issue --- .github/workflows/update-os.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index cb9066f..9522d87 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -25,8 +25,8 @@ jobs: with: # Set the base_image to the desired Raspberry Pi OS version base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz - image_additional_mb: 3584 # enlarge free space to 3.5 GB - optimize_image: true + image_additional_mb: 3072 # enlarge free space to 3 GB + optimize_image: false commands: | cd /home sudo useradd -m -p "$(openssl passwd -1 $INKY_INITAL_PASSWORD)" inky From 995a66b15f7def822b9f056ab099c2c4729285da Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 02:29:14 +0200 Subject: [PATCH 43/62] unmounting issues --- .github/workflows/update-os.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 9522d87..958087a 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -26,7 +26,7 @@ jobs: # Set the base_image to the desired Raspberry Pi OS version base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz image_additional_mb: 3072 # enlarge free space to 3 GB - optimize_image: false + optimize_image: true commands: | cd /home sudo useradd -m -p "$(openssl passwd -1 $INKY_INITAL_PASSWORD)" inky @@ -88,7 +88,7 @@ jobs: # temporarily disabled to allow pisugar support # echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password # allow some time to unmount - sleep 10 + sleep 30 - name: Compress the release image run: | From 2965f4f21801ad6abcecc1eb950350cdf08571d6 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 2 Jul 2024 02:51:01 +0200 Subject: [PATCH 44/62] fixing bugs.. --- .github/workflows/update-os.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 958087a..2672458 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -69,10 +69,11 @@ jobs: echo $CWD # increase swap-size - sudo dphys-swapfile swapoff - sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile - sudo dphys-swapfile setup - sudo dphys-swapfile swapon + # temporarily disabled due to unmounting issues + # sudo dphys-swapfile swapoff + # sudo sed -i -E '/^CONF_SWAPSIZE=/s/=.*/=512/' /etc/dphys-swapfile + # sudo dphys-swapfile setup + # sudo dphys-swapfile swapon # enable SPI sudo sed -i s/#dtparam=spi=on/dtparam=spi=on/ /boot/config.txt @@ -88,7 +89,7 @@ jobs: # temporarily disabled to allow pisugar support # echo 'ALL ALL=(ALL:ALL) PASSWD: ALL' | sudo tee -a /etc/sudoers.d/010_require_sudo_password # allow some time to unmount - sleep 30 + sleep 10 - name: Compress the release image run: | From 2c06a2f979618ca99fba191cee0920d1dbb71744 Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 4 Jul 2024 23:43:52 +0200 Subject: [PATCH 45/62] Fix typos --- inkycal/main.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index bf2fe86..0934204 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -137,11 +137,8 @@ class Inkycal: except: logger.exception(f"Exception: {traceback.format_exc()}.") - # Path to store images - self.image_folder = settings.IMAGE_FOLDER - # Remove old hashes - self._remove_hashes(self.image_folder) + self._remove_hashes(settings.IMAGE_FOLDER) # set up cache if not os.path.exists(os.path.join(settings.CACHE_PATH, CACHE_NAME)): @@ -352,11 +349,11 @@ class Inkycal: self._calibration_check() if self._calibration_state: # After calibration, we have to forcefully rewrite the screen - self._remove_hashes(self.image_folder) + self._remove_hashes(settings.IMAGE_FOLDER) if self.supports_colour: - im_black = Image.open(f"{self.image_folder}canvas.png") - im_colour = Image.open(f"{self.image_folder}canvas_colour.png") + im_black = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas.png")) + im_colour = Image.open(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png")) # Flip the image by 180° if required if self.settings['orientation'] == 180: @@ -365,8 +362,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"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), + (f"{settings.IMAGE_FOLDER}/canvas_colour.png.hash", im_colour) ]): display.render(im_black, im_colour) @@ -379,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"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), ]): display.render(im_black) logger.info(f'\nNo errors since {self.counter} display updates') @@ -415,8 +412,8 @@ class Inkycal: returns the merged image """ - im1_path = os.path.join(settings.image_folder, "canvas.png") - im2_path = os.path.join(settings.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): @@ -454,8 +451,8 @@ class Inkycal: for number in range(1, self._module_number): # get the path of the current module's generated images - im1_path = f"{self.image_folder}module{number}_black.png" - im2_path = f"{self.image_folder}module{number}_colour.png" + im1_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png") + im2_path = os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png") # Check if there is an image for the black band if os.path.exists(im1_path): @@ -525,8 +522,8 @@ class Inkycal: im_black = self._optimize_im(im_black) im_colour = self._optimize_im(im_colour) - im_black.save(self.image_folder + 'canvas.png', 'PNG') - im_colour.save(self.image_folder + 'canvas_colour.png', 'PNG') + im_black.save(os.path.join(settings.IMAGE_FOLDER, "canvas.png"), "PNG") + im_colour.save(os.path.join(settings.IMAGE_FOLDER, "canvas_colour.png"), 'PNG') # Additionally, combine the two images with color def clear_white(img): @@ -614,8 +611,8 @@ class Inkycal: 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") + black.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_black.png"), "PNG") + colour.save(os.path.join(settings.IMAGE_FOLDER, f"module{number}_colour.png"), "PNG") return True except Exception: logger.exception(f"Error in module {number}!") From 1ee7995a751c0e91bd654aa091dbbf42c2dc163f Mon Sep 17 00:00:00 2001 From: Ace Date: Fri, 5 Jul 2024 11:54:07 +0200 Subject: [PATCH 46/62] adjust parallel drivers path --- inkycal/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkycal/settings.py b/inkycal/settings.py index 8f40352..7d6be51 100644 --- a/inkycal/settings.py +++ b/inkycal/settings.py @@ -15,6 +15,6 @@ class Settings: 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") + PARALLEL_DRIVER_PATH = os.path.join(basedir, "display", "drivers", "parallel_drivers") TEMPORARY_FOLDER = os.path.join(basedir, "tmp") VCOM = "2.0" From c45b74a4d059457e0598d96ad78a23a0a6294c46 Mon Sep 17 00:00:00 2001 From: Ace Date: Fri, 5 Jul 2024 13:20:57 +0200 Subject: [PATCH 47/62] fix parallel drivers not finding the image --- inkycal/display/drivers/10_in_3.py | 4 ++-- inkycal/display/drivers/7_in_8.py | 4 ++-- inkycal/display/drivers/9_in_7.py | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/inkycal/display/drivers/10_in_3.py b/inkycal/display/drivers/10_in_3.py index 7d7b3fa..ee37e92 100644 --- a/inkycal/display/drivers/10_in_3.py +++ b/inkycal/display/drivers/10_in_3.py @@ -36,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(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"}' + image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP") + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(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 5f9cf51..9e700c7 100644 --- a/inkycal/display/drivers/7_in_8.py +++ b/inkycal/display/drivers/7_in_8.py @@ -34,8 +34,8 @@ class EPD: def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True) - 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"}' + image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), 'BMP') + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(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 c81feef..f801dcf 100644 --- a/inkycal/display/drivers/9_in_7.py +++ b/inkycal/display/drivers/9_in_7.py @@ -2,6 +2,7 @@ 9.7" driver class Copyright by aceinnolab """ +import os from subprocess import run from inkycal.settings import Settings @@ -10,11 +11,8 @@ from inkycal.settings import Settings EPD_WIDTH = 1200 EPD_HEIGHT = 825 - settings = Settings() -command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}' - class EPD: @@ -36,8 +34,8 @@ class EPD: def getbuffer(self, image): """ad-hoc""" image = image.rotate(90, expand=True) - 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"}' + image.convert("RGB").save(os.path.join(settings.IMAGE_FOLDER, "canvas.bmp"), "BMP") + command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {os.path.join(settings.IMAGE_FOLDER, "canvas.bmp")}' print(command) return command From 8f5639afcf268961888b32b377507c2c542fd601 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 01:31:00 +0000 Subject: [PATCH 48/62] Bump certifi from 2024.2.2 to 2024.7.4 Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3d18e25..de52755 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ appdirs==1.4.4 arrow==1.3.0 asyncio==3.4.3 beautifulsoup4==4.12.3 -certifi==2024.2.2 +certifi==2024.7.4 cfgv==3.4.0 charset-normalizer==3.3.2 colorzero==2.0 From e2cc46bb1578d3fdacb467797ae180358a6a0348 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 7 Jul 2024 19:52:29 +0200 Subject: [PATCH 49/62] fix log path --- inkycal/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkycal/settings.py b/inkycal/settings.py index 7d6be51..9623508 100644 --- a/inkycal/settings.py +++ b/inkycal/settings.py @@ -11,7 +11,7 @@ class Settings: """ CACHE_PATH = os.path.join(basedir, "cache") - LOG_PATH = os.path.join(basedir, "logs") + 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") From 4f4553ff218ff99d79b5b3b1af6cbea907e62ec4 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 7 Jul 2024 19:53:43 +0200 Subject: [PATCH 50/62] remove newline from log line --- inkycal/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkycal/main.py b/inkycal/main.py index 0934204..1275066 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -379,7 +379,7 @@ class Inkycal: (f"{settings.IMAGE_FOLDER}/canvas.png.hash", im_black), ]): display.render(im_black) - logger.info(f'\nNo errors since {self.counter} display updates') + logger.info(f'No errors since {self.counter} display updates') logger.info(f'program started {runtime.humanize()}') # store the cache data From b2a8dc126c626d332a3feee644edff38214b8e46 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 13:53:41 +0200 Subject: [PATCH 51/62] test adding support for 13.3" SPI display --- inkycal/display/drivers/epd_13_in_3.py | 527 ++++++++++++++++++ inkycal/display/drivers/epd_13_in_3_colour.py | 299 ++++++++++ inkycal/display/supported_models.py | 2 + 3 files changed, 828 insertions(+) create mode 100644 inkycal/display/drivers/epd_13_in_3.py create mode 100644 inkycal/display/drivers/epd_13_in_3_colour.py diff --git a/inkycal/display/drivers/epd_13_in_3.py b/inkycal/display/drivers/epd_13_in_3.py new file mode 100644 index 0000000..4ed8a24 --- /dev/null +++ b/inkycal/display/drivers/epd_13_in_3.py @@ -0,0 +1,527 @@ +""" +* | File : epd13in3k.py +* | Author : Waveshare team +* | Function : Electronic paper driver +* | Info : +*---------------- +* | This version: V1.0 +* | Date : 2023-09-08 +# | Info : python demo +----------------------------------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import logging + +from inkycal.display.drivers import epdconfig + +# Display resolution +EPD_WIDTH = 960 +EPD_HEIGHT = 680 + +GRAY1 = 0xff # white +GRAY2 = 0xC0 +GRAY3 = 0x80 # gray +GRAY4 = 0x00 # Blackest + +logger = logging.getLogger(__name__) + + +class EPD: + def __init__(self): + self.reset_pin = epdconfig.RST_PIN + self.dc_pin = epdconfig.DC_PIN + self.busy_pin = epdconfig.BUSY_PIN + self.cs_pin = epdconfig.CS_PIN + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + self.GRAY1 = GRAY1 # white + self.GRAY2 = GRAY2 + self.GRAY3 = GRAY3 # gray + self.GRAY4 = GRAY4 # Blackest + + self.Lut_Partial = [ + 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x2A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x15, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x01, 0x01, 0x00, + 0x0A, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x01, + 0x22, 0x22, 0x22, 0x22, 0x22, + 0x17, 0x41, 0xA8, 0x32, 0x18, + 0x00, 0x00, ] + + self.LUT_DATA_4Gray = [ + 0x80, 0x48, 0x4A, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0A, 0x48, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x88, 0x48, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xA8, 0x48, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x23, 0x17, 0x02, 0x00, + 0x05, 0x01, 0x05, 0x01, 0x02, + 0x08, 0x02, 0x01, 0x04, 0x04, + 0x00, 0x02, 0x00, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, + 0x22, 0x22, 0x22, 0x22, 0x22, + 0x17, 0x41, 0xA8, 0x32, 0x30, + 0x00, 0x00, ] + + if (epdconfig.module_init() != 0): + return -1 + + # Hardware reset + def reset(self): + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + epdconfig.digital_write(self.reset_pin, 0) + epdconfig.delay_ms(2) + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + + def send_command(self, command): + epdconfig.digital_write(self.dc_pin, 0) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([command]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([data]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data2(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.SPI.writebytes2(data) + epdconfig.digital_write(self.cs_pin, 1) + + def ReadBusy(self): + logger.debug("e-Paper busy") + busy = epdconfig.digital_read(self.busy_pin) + while (busy == 1): + busy = epdconfig.digital_read(self.busy_pin) + epdconfig.delay_ms(20) + epdconfig.delay_ms(20) + logger.debug("e-Paper busy release") + + def TurnOnDisplay(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xF7) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def TurnOnDisplay_Part(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xCF) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def TurnOnDisplay_4GRAY(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xC7) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def Lut(self, LUT): + self.send_command(0x32) + for i in range(105): + self.send_data(LUT[i]) + + self.send_command(0x03) + self.send_data(LUT[105]) + + self.send_command(0x04) + self.send_data(LUT[106]) + self.send_data(LUT[107]) + self.send_data(LUT[108]) + + self.send_command(0x2C) + self.send_data(LUT[109]) + + def init(self): + + # EPD hardware init start + self.reset() + self.ReadBusy() + + self.send_command(0x12) # SWRESET + self.ReadBusy() + + self.send_command(0x0C) + self.send_data(0xAE) + self.send_data(0xC7) + self.send_data(0xC3) + self.send_data(0xC0) + self.send_data(0x80) + + self.send_command(0x01) + self.send_data(0xA7) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x11) + self.send_data(0x03) + + self.send_command(0x44) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xBF) + self.send_data(0x03) + + self.send_command(0x45) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xA7) + self.send_data(0x02) + + self.send_command(0x3C) + self.send_data(0x05) + + self.send_command(0x18) + self.send_data(0x80) + + self.send_command(0x4E) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x4F) + self.send_data(0x00) + self.send_data(0x00) + + # EPD hardware init end + return 0 + + def init_Part(self): + self.reset() + + self.send_command(0x3C) + self.send_data(0x80) + + self.Lut(self.Lut_Partial) + + self.send_command(0x37) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x40) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x3C) + self.send_data(0x80) + + self.send_command(0x22) + self.send_data(0xC0) + self.send_command(0x20) + + self.ReadBusy() + + def init_4GRAY(self): + self.reset() + + self.ReadBusy() + self.send_command(0x12) + self.ReadBusy() + + self.send_command(0x0C) + self.send_data(0xAE) + self.send_data(0xC7) + self.send_data(0xC3) + self.send_data(0xC0) + self.send_data(0x80) + + self.send_command(0x01) + self.send_data(0xA7) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x11) + self.send_data(0x03) + + self.send_command(0x44) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xBF) + self.send_data(0x03) + + self.send_command(0x45) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xA7) + self.send_data(0x02) + + self.send_command(0x3C) + self.send_data(0x00) + + self.send_command(0x18) + self.send_data(0x80) + + self.send_command(0x4E) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x4F) + self.send_data(0x00) + self.send_data(0x00) + + self.Lut(self.LUT_DATA_4Gray) + + self.ReadBusy() + + def getbuffer(self, image): + # logger.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width / 8) * self.height) + image_monocolor = image.convert('1') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + # logger.debug("imwidth = %d, imheight = %d",imwidth,imheight) + if imwidth == self.width and imheight == self.height: + logger.debug("Horizontal") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] == 0: + buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) + elif imwidth == self.height and imheight == self.width: + logger.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + newx = y + newy = self.height - x - 1 + if pixels[x, y] == 0: + buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8)) + return buf + + def getbuffer_4Gray(self, image): + # logger.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width / 4) * self.height) + image_monocolor = image.convert('L') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + i = 0 + # logger.debug("imwidth = %d, imheight = %d",imwidth,imheight) + if (imwidth == self.width and imheight == self.height): + logger.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if (pixels[x, y] == 0xC0): + pixels[x, y] = 0x80 + elif (pixels[x, y] == 0x80): + pixels[x, y] = 0x40 + i = i + 1 + if (i % 4 == 0): + buf[int((x + (y * self.width)) / 4)] = ( + (pixels[x - 3, y] & 0xc0) | (pixels[x - 2, y] & 0xc0) >> 2 | ( + pixels[x - 1, y] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6) + + elif (imwidth == self.height and imheight == self.width): + logger.debug("Horizontal") + for x in range(imwidth): + for y in range(imheight): + newx = y + newy = self.height - x - 1 + if (pixels[x, y] == 0xC0): + pixels[x, y] = 0x80 + elif (pixels[x, y] == 0x80): + pixels[x, y] = 0x40 + i = i + 1 + if (i % 4 == 0): + buf[int((newx + (newy * self.width)) / 4)] = ( + (pixels[x, y - 3] & 0xc0) | (pixels[x, y - 2] & 0xc0) >> 2 | ( + pixels[x, y - 1] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6) + return buf + + def Clear(self): + buf = [0xFF] * (int(self.width / 8) * self.height) + self.send_command(0x24) + self.send_data2(buf) + + self.TurnOnDisplay() + + def display(self, image): + self.send_command(0x24) + self.send_data2(image) + + self.TurnOnDisplay() + + def display_Base(self, image): + self.send_command(0x24) + self.send_data2(image) + + self.send_command(0x26) + self.send_data2(image) + + self.TurnOnDisplay() + + def display_Base_color(self, color): + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + self.send_command(0x24) # Write Black and White image to RAM + for j in range(Height): + for i in range(Width): + self.send_data(color) + + self.send_command(0x26) # Write Black and White image to RAM + for j in range(Height): + for i in range(Width): + self.send_data(color) + # self.TurnOnDisplay() + + def display_Partial(self, Image, Xstart, Ystart, Xend, Yend): + if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | ( + Xend - Xstart) % 8 == 0): + Xstart = Xstart // 8 + Xend = Xend // 8 + else: + Xstart = Xstart // 8 + if Xend % 8 == 0: + Xend = Xend // 8 + else: + Xend = Xend // 8 + 1 + + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + + Xend -= 1 + Yend -= 1 + + self.send_command(0x44) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_data((Xend * 8) & 0xff) + self.send_data((Xend >> 5) & 0x01) + self.send_command(0x45) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + self.send_data(Yend & 0xff) + self.send_data((Yend >> 8) & 0x01) + + self.send_command(0x4E) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_command(0x4F) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + + self.send_command(0x24) + for j in range(Height): + for i in range(Width): + if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))): + self.send_data(Image[i + j * Width]) + self.TurnOnDisplay_Part() + + def display_4Gray(self, image): + self.send_command(0x24) + for i in range(0, 81600): + temp3 = 0 + for j in range(0, 2): + temp1 = image[i * 2 + j] + for k in range(0, 2): + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x01 + else: # 0x40 + temp3 |= 0x00 + temp3 <<= 1 + + temp1 <<= 2 + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x01 + else: # 0x40 + temp3 |= 0x00 + if (j != 1 or k != 1): + temp3 <<= 1 + temp1 <<= 2 + self.send_data(temp3) + + self.send_command(0x26) + for i in range(0, 81600): + temp3 = 0 + for j in range(0, 2): + temp1 = image[i * 2 + j] + for k in range(0, 2): + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x00 + else: # 0x40 + temp3 |= 0x01 + temp3 <<= 1 + + temp1 <<= 2 + temp2 = temp1 & 0xC0 + if (temp2 == 0xC0): + temp3 |= 0x00 + elif (temp2 == 0x00): + temp3 |= 0x01 + elif (temp2 == 0x80): + temp3 |= 0x00 + else: # 0x40 + temp3 |= 0x01 + if (j != 1 or k != 1): + temp3 <<= 1 + temp1 <<= 2 + self.send_data(temp3) + + self.TurnOnDisplay_4GRAY() + + def sleep(self): + self.send_command(0x10) # DEEP_SLEEP + self.send_data(0x03) + + epdconfig.delay_ms(2000) + epdconfig.module_exit() diff --git a/inkycal/display/drivers/epd_13_in_3_colour.py b/inkycal/display/drivers/epd_13_in_3_colour.py new file mode 100644 index 0000000..663d88f --- /dev/null +++ b/inkycal/display/drivers/epd_13_in_3_colour.py @@ -0,0 +1,299 @@ +""" +* | File : epd13in3b.py +* | Author : Waveshare team +* | Function : Electronic paper driver +* | Info : +*---------------- +* | This version: V1.0 +* | Date : 2024-04-08 +# | Info : python demo +----------------------------------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import logging + +from inkycal.display.drivers import epdconfig + +# Display resolution +EPD_WIDTH = 960 +EPD_HEIGHT = 680 + +GRAY1 = 0xff # white +GRAY2 = 0xC0 +GRAY3 = 0x80 # gray +GRAY4 = 0x00 # Blackest + +logger = logging.getLogger(__name__) + + +class EPD: + def __init__(self): + self.reset_pin = epdconfig.RST_PIN + self.dc_pin = epdconfig.DC_PIN + self.busy_pin = epdconfig.BUSY_PIN + self.cs_pin = epdconfig.CS_PIN + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + if (epdconfig.module_init() != 0): + return -1 + + # Hardware reset + def reset(self): + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + epdconfig.digital_write(self.reset_pin, 0) + epdconfig.delay_ms(2) + epdconfig.digital_write(self.reset_pin, 1) + epdconfig.delay_ms(20) + + def send_command(self, command): + epdconfig.digital_write(self.dc_pin, 0) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([command]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.spi_writebyte([data]) + epdconfig.digital_write(self.cs_pin, 1) + + def send_data2(self, data): + epdconfig.digital_write(self.dc_pin, 1) + epdconfig.digital_write(self.cs_pin, 0) + epdconfig.SPI.writebytes2(data) + epdconfig.digital_write(self.cs_pin, 1) + + def ReadBusy(self): + logger.debug("e-Paper busy") + busy = epdconfig.digital_read(self.busy_pin) + while (busy == 1): + busy = epdconfig.digital_read(self.busy_pin) + epdconfig.delay_ms(20) + epdconfig.delay_ms(20) + logger.debug("e-Paper busy release") + + def TurnOnDisplay(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xF7) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def TurnOnDisplay_Part(self): + self.send_command(0x22) # Display Update Control + self.send_data(0xFF) + self.send_command(0x20) # Activate Display Update Sequence + self.ReadBusy() + + def init(self): + # EPD hardware init start + self.reset() + self.ReadBusy() + + self.send_command(0x12) # SWRESET + self.ReadBusy() + + self.send_command(0x0C) + self.send_data(0xAE) + self.send_data(0xC7) + self.send_data(0xC3) + self.send_data(0xC0) + self.send_data(0x80) + + self.send_command(0x01) + self.send_data(0xA7) + self.send_data(0x02) + self.send_data(0x00) + + self.send_command(0x11) + self.send_data(0x03) + + self.send_command(0x44) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xBF) + self.send_data(0x03) + + self.send_command(0x45) + self.send_data(0x00) + self.send_data(0x00) + self.send_data(0xA7) + self.send_data(0x02) + + self.send_command(0x3C) + self.send_data(0x01) + + self.send_command(0x18) + self.send_data(0x80) + + self.send_command(0x4E) + self.send_data(0x00) + self.send_data(0x00) + + self.send_command(0x4F) + self.send_data(0x00) + self.send_data(0x00) + self.ReadBusy() + + # EPD hardware init end + return 0 + + def getbuffer(self, image): + # logger.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width / 8) * self.height) + image_monocolor = image.convert('1') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + # logger.debug("imwidth = %d, imheight = %d",imwidth,imheight) + if imwidth == self.width and imheight == self.height: + logger.debug("Horizontal") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] == 0: + buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) + elif imwidth == self.height and imheight == self.width: + logger.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + newx = y + newy = self.height - x - 1 + if pixels[x, y] == 0: + buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8)) + return buf + + def Clear(self): + self.send_command(0x24) + self.send_data2([0xFF] * (int(self.width / 8) * self.height)) + self.send_command(0x26) + self.send_data2([0x00] * (int(self.width / 8) * self.height)) + + self.TurnOnDisplay() + + def Clear_Base(self): + self.send_command(0x24) + self.send_data2([0xFF] * (int(self.width / 8) * self.height)) + self.send_command(0x26) + self.send_data2([0x00] * (int(self.width / 8) * self.height)) + + self.TurnOnDisplay() + self.send_command(0x26) + self.send_data2([0xFF] * (int(self.width / 8) * self.height)) + + def display(self, blackimage, ryimage): + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + if (blackimage != None): + self.send_command(0x24) + self.send_data2(blackimage) + if (ryimage != None): + for j in range(Height): + for i in range(Width): + ryimage[i + j * Width] = ~ryimage[i + j * Width] + self.send_command(0x26) + self.send_data2(ryimage) + + self.TurnOnDisplay() + + def display_Base(self, blackimage, ryimage): + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + if (blackimage != None): + self.send_command(0x24) + self.send_data2(blackimage) + if (ryimage != None): + for j in range(Height): + for i in range(Width): + ryimage[i + j * Width] = ~ryimage[i + j * Width] + self.send_command(0x26) + self.send_data2(ryimage) + + self.TurnOnDisplay() + + self.send_command(0x26) + self.send_data2(blackimage) + + def display_Partial(self, Image, Xstart, Ystart, Xend, Yend): + if ((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | ( + Xend - Xstart) % 8 == 0): + Xstart = Xstart // 8 + Xend = Xend // 8 + else: + Xstart = Xstart // 8 + if Xend % 8 == 0: + Xend = Xend // 8 + else: + Xend = Xend // 8 + 1 + + if (self.width % 8 == 0): + Width = self.width // 8 + else: + Width = self.width // 8 + 1 + Height = self.height + + Xend -= 1 + Yend -= 1 + + self.send_command(0x3C) + self.send_data(0x80) + + self.send_command(0x44) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_data((Xend * 8) & 0xff) + self.send_data((Xend >> 5) & 0x01) + self.send_command(0x45) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + self.send_data(Yend & 0xff) + self.send_data((Yend >> 8) & 0x01) + + self.send_command(0x4E) + self.send_data((Xstart * 8) & 0xff) + self.send_data((Xstart >> 5) & 0x01) + self.send_command(0x4F) + self.send_data(Ystart & 0xff) + self.send_data((Ystart >> 8) & 0x01) + + self.send_command(0x24) + for j in range(Height): + for i in range(Width): + if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))): + self.send_data(Image[i + j * Width]) + self.TurnOnDisplay_Part() + + self.send_command(0x26) + for j in range(Height): + for i in range(Width): + if ((j > Ystart - 1) & (j < (Yend + 1)) & (i > Xstart - 1) & (i < (Xend + 1))): + self.send_data(Image[i + j * Width]) + + def sleep(self): + self.send_command(0x10) # DEEP_SLEEP + self.send_data(0x03) + + epdconfig.delay_ms(2000) + epdconfig.module_exit() diff --git a/inkycal/display/supported_models.py b/inkycal/display/supported_models.py index cbf535c..f8a9fb4 100644 --- a/inkycal/display/supported_models.py +++ b/inkycal/display/supported_models.py @@ -1,4 +1,6 @@ supported_models = { + "epd_13_in_3": (960, 680), + "epd_13_in_3_colour": (960, 680), "epd_12_in_48": (1304, 984), "epd_7_in_5_colour": (640, 384), "9_in_7": (1200, 825), From 2bfe09e54b7337c77fce069472f24f62177a5457 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 16:28:22 +0200 Subject: [PATCH 52/62] Use bullseye image as bookworm still has issues --- .github/workflows/update-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 2672458..ca9872a 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -24,7 +24,7 @@ jobs: TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }} with: # Set the base_image to the desired Raspberry Pi OS version - base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz + base_image: https://downloads.raspberrypi.com/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2024-07-04/2024-07-04-raspios-bullseye-armhf-lite.img.xz image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | From db5e279fda15cb638740f1c941a820cf69c2d294 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 17:53:09 +0200 Subject: [PATCH 53/62] old legacy stable image is also no-good --- .github/workflows/update-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index ca9872a..06e17cc 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -24,7 +24,7 @@ jobs: TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }} with: # Set the base_image to the desired Raspberry Pi OS version - base_image: https://downloads.raspberrypi.com/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2024-07-04/2024-07-04-raspios-bullseye-armhf-lite.img.xz + base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/ image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | From 5c68b02e0970bf4513e8d8b83e8b2f89fc913b80 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 18:51:07 +0200 Subject: [PATCH 54/62] old legacy stable image is also no-good --- tests/test_inkycal_weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inkycal_weather.py b/tests/test_inkycal_weather.py index 2325616..dce3297 100755 --- a/tests/test_inkycal_weather.py +++ b/tests/test_inkycal_weather.py @@ -30,7 +30,7 @@ tests = [ "forecast_interval": "daily", "units": "metric", "hour_format": "12", - "use_beaufort": True, + "use_beaufort": False, "padding_x": 10, "padding_y": 10, "fontsize": 12, From 0ff3e4c2ed836a1cf21dc285040b954a50efeba2 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 19:08:00 +0200 Subject: [PATCH 55/62] test last supported os --- .github/workflows/update-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 2672458..8057fe4 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -24,7 +24,7 @@ jobs: TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }} with: # Set the base_image to the desired Raspberry Pi OS version - base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2024-03-15/2024-03-15-raspios-bookworm-armhf-lite.img.xz + base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-10-10/ image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | From 4dc5eb2c2595c1ec7a7b3008fca2d12dfa6eed43 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 19:10:53 +0200 Subject: [PATCH 56/62] fix os link --- .github/workflows/update-os.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 8057fe4..51c4f91 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -24,7 +24,7 @@ jobs: TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }} with: # Set the base_image to the desired Raspberry Pi OS version - base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-10-10/ + base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/2023-12-11-raspios-bookworm-armhf-lite.img.xz image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | From 1d9bf8e22eb4e5e413d2e6fe70aa24dadf5d5118 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 9 Jul 2024 20:59:48 +0200 Subject: [PATCH 57/62] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e7d54b1..2e43063 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Welcome to inkycal v2.0.3! +# Welcome to inkycal v2.0.4!

From 5b8503fcc90e99d140be856a37ddf28ead60fe53 Mon Sep 17 00:00:00 2001 From: Ace Date: Wed, 10 Jul 2024 21:03:00 +0200 Subject: [PATCH 58/62] added note about bookworm --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 2e43063..e465de6 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,7 @@ display!** ## Configuring the Raspberry Pi -Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). -Use the following settings: +Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/2023-12-11-raspios-bookworm-armhf-lite.img.xz) as the latest release is known to have some issues with the latest kernel update. | option | value | |:--------------------------|:---------------------------:| From f4fee0e31f9b1d351b35b25d1586e6214a5be8fa Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 18 Jul 2024 01:43:25 +0200 Subject: [PATCH 59/62] fix boot folder remapping --- inkycal/main.py | 18 +++++++++++------- inkycal/settings.py | 2 ++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index 1275066..cef0e11 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -6,6 +6,7 @@ Copyright by aceinnolab import asyncio import glob import hashlib +import os.path import numpy @@ -72,13 +73,16 @@ class Inkycal: f"No settings.json file could be found in the specified location: {settings_path}") else: - logger.info("Looking for settings.json file in /boot folder...") - try: - with open('/boot/settings.json', mode="r") as settings_file: - self.settings = json.load(settings_file) - - except FileNotFoundError: - raise SettingsFileNotFoundError + found = False + for location in settings.SETTINGS_JSON_PATHS: + if os.path.exists(location): + logger.info(f"Found settings.json file in {location}") + with open(location, mode="r") as settings_file: + self.settings = json.load(settings_file) + found = True + break + if not found: + raise SettingsFileNotFoundError(f"No settings.json file could be found in {settings.SETTINGS_JSON_PATHS} and no explicit path was specified.") self.disable_calibration = self.settings.get('disable_calibration', False) if self.disable_calibration: diff --git a/inkycal/settings.py b/inkycal/settings.py index 9623508..9c4b9dc 100644 --- a/inkycal/settings.py +++ b/inkycal/settings.py @@ -18,3 +18,5 @@ class Settings: PARALLEL_DRIVER_PATH = os.path.join(basedir, "display", "drivers", "parallel_drivers") TEMPORARY_FOLDER = os.path.join(basedir, "tmp") VCOM = "2.0" + # /boot/settings.json is path on older releases, while the latter is more the more recent ones + SETTINGS_JSON_PATHS = ["/boot/settings.json", "/boot/firmware/settings.json"] From e5bd164c4e94d2cfc91154f7edb4e67ea9ca6204 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 17 Jul 2024 23:44:34 +0000 Subject: [PATCH 60/62] update docs [bot] --- docs/_static/searchtools.js | 7 ++++--- docs/searchindex.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 92da3f8..b08d58c 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -178,7 +178,7 @@ const Search = { htmlToText: (htmlString, anchor) => { const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - for (const removalQuery of [".headerlinks", "script", "style"]) { + for (const removalQuery of [".headerlink", "script", "style"]) { htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); } if (anchor) { @@ -328,13 +328,14 @@ const Search = { for (const [title, foundTitles] of Object.entries(allTitles)) { if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { for (const [file, id] of foundTitles) { - let score = Math.round(100 * queryLower.length / title.length) + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, id !== null ? "#" + id : "", null, - score, + score + boost, filenames[file], ]); } diff --git a/docs/searchindex.js b/docs/searchindex.js index 9b7ecaf..0015f4d 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"About Inkycal": [[0, "about-inkycal"]], "Contents:": [[2, null]], "Creating settings file": [[4, "creating-settings-file"]], "Custom functions": [[3, "module-inkycal.custom.functions"]], "Developer documentation": [[1, "developer-documentation"]], "Display": [[3, "module-inkycal.display.Display"]], "Helper classes": [[3, "module-inkycal.modules.ical_parser"]], "Indices and tables": [[2, "indices-and-tables"]], "Inkycal": [[3, "module-inkycal.main"]], "Inkycal documentation": [[2, "inkycal-documentation"]], "Installing Inkycal": [[4, "installing-inkycal"]], "Quickstart": [[4, "quickstart"]]}, "docnames": ["about", "dev_doc", "index", "inkycal", "quickstart"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["about.md", "dev_doc.md", "index.rst", "inkycal.rst", "quickstart.md"], "indexentries": {"all_day() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.all_day", false]], "auto_fontsize() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.auto_fontsize", false]], "autoflip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.autoflip", false]], "calibrate() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.calibrate", false]], "clear() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.clear", false]], "clear_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.clear_events", false]], "countdown() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.countdown", false]], "draw_border() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.draw_border", false]], "dry_run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.dry_run", false]], "flip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.flip", false]], "get_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_events", false]], "get_fonts() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_fonts", false]], "get_system_tz() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_system_tz", false]], "get_system_tz() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_system_tz", false]], "icalendar (class in inkycal.modules.ical_parser)": [[3, "inkycal.modules.ical_parser.iCalendar", false]], "image_to_palette() (in module inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.image_to_palette", false]], "inkycal (class in inkycal.main)": [[3, "inkycal.main.Inkycal", false]], "inkycal.custom.functions": [[3, "module-inkycal.custom.functions", false]], "inkycal.display.display": [[3, "module-inkycal.display.Display", false]], "inkycal.main": [[3, "module-inkycal.main", false]], "inkycal.modules.ical_parser": [[3, "module-inkycal.modules.ical_parser", false]], "inkycal.modules.inky_image": [[3, "module-inkycal.modules.inky_image", false]], "inkyimage (class in inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.Inkyimage", false]], "internet_available() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.internet_available", false]], "load() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.load", false]], "load_from_file() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_from_file", false]], "load_url() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_url", false]], "merge() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.merge", false]], "module": [[3, "module-inkycal.custom.functions", false], [3, "module-inkycal.display.Display", false], [3, "module-inkycal.main", false], [3, "module-inkycal.modules.ical_parser", false], [3, "module-inkycal.modules.inky_image", false]], "preview() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.preview", false]], "process_module() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.process_module", false]], "remove_alpha() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.remove_alpha", false]], "resize() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.resize", false]], "run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.run", false]], "show_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.show_events", false]], "sort() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.sort", false]], "text_wrap() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.text_wrap", false]], "write() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.write", false]]}, "objects": {"inkycal": [[3, 0, 0, "-", "main"]], "inkycal.custom": [[3, 0, 0, "-", "functions"]], "inkycal.custom.functions": [[3, 1, 1, "", "auto_fontsize"], [3, 1, 1, "", "draw_border"], [3, 1, 1, "", "get_fonts"], [3, 1, 1, "", "get_system_tz"], [3, 1, 1, "", "internet_available"], [3, 1, 1, "", "text_wrap"], [3, 1, 1, "", "write"]], "inkycal.display": [[3, 0, 0, "-", "Display"]], "inkycal.main": [[3, 2, 1, "", "Inkycal"]], "inkycal.main.Inkycal": [[3, 3, 1, "", "calibrate"], [3, 3, 1, "", "countdown"], [3, 3, 1, "", "dry_run"], [3, 3, 1, "", "process_module"], [3, 3, 1, "", "run"]], "inkycal.modules": [[3, 0, 0, "-", "ical_parser"], [3, 0, 0, "-", "inky_image"]], "inkycal.modules.ical_parser": [[3, 2, 1, "", "iCalendar"]], "inkycal.modules.ical_parser.iCalendar": [[3, 3, 1, "", "all_day"], [3, 3, 1, "", "clear_events"], [3, 3, 1, "", "get_events"], [3, 3, 1, "", "get_system_tz"], [3, 3, 1, "", "load_from_file"], [3, 3, 1, "", "load_url"], [3, 3, 1, "", "show_events"], [3, 3, 1, "", "sort"]], "inkycal.modules.inky_image": [[3, 2, 1, "", "Inkyimage"], [3, 1, 1, "", "image_to_palette"]], "inkycal.modules.inky_image.Inkyimage": [[3, 3, 1, "", "autoflip"], [3, 3, 1, "", "clear"], [3, 3, 1, "", "flip"], [3, 3, 1, "", "load"], [3, 3, 1, "", "merge"], [3, 3, 1, "", "preview"], [3, 3, 1, "", "remove_alpha"], [3, 3, 1, "", "resize"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"": [0, 3], "0": 3, "1": 3, "10": 3, "100": 3, "11": 3, "16": 3, "16grai": 3, "180": 3, "2": 3, "20": 3, "270": 3, "3": 3, "32": 3, "360": 3, "5": 3, "7": 3, "80": 3, "9": 3, "90": 3, "A": 3, "If": 3, "In": 3, "It": 0, "No": 0, "OR": 3, "The": [0, 3], "Then": 3, "To": 3, "_": 0, "about": 2, "access": 3, "accur": 3, "aceinnolab": [3, 4], "aceisac": 0, "actual": 3, "ad": 0, "add": 3, "adjust": 3, "after": 3, "agenda": 0, "align": 3, "aliv": 0, "all": [0, 3], "all_dai": 3, "allow": 3, "alpha": 3, "also": 0, "amount": 0, "an": 3, "angl": 3, "anti": 3, "anyth": 0, "arg": 3, "arrow": 3, "assembl": 3, "async": 3, "atom": 0, "attempt": 3, "attribut": 3, "auto_fonts": [2, 3], "autofit": 3, "autoflip": 3, "automat": 3, "avail": 3, "awar": 3, "band": 3, "base": 3, "befor": 3, "begin": 3, "behind": 0, "below": 3, "black": 3, "blend": 0, "bool": 3, "boot": 3, "border": 3, "box": 3, "box_siz": 3, "built": 0, "bw": 3, "bwr": 3, "bwy": 3, "calcul": 3, "calendar": 0, "calibr": 3, "can": [0, 3], "care": [0, 3], "case": 0, "caus": 3, "cd": 4, "center": 3, "chang": 3, "check": 3, "choos": 3, "chunk": 3, "class": 2, "clear": 3, "clear_ev": 3, "clockwis": 3, "clone": 4, "co": 3, "coffe": 0, "colour": 3, "com": [3, 4], "come": 4, "commerci": 0, "commonli": 3, "commun": 0, "compat": 0, "config": 3, "connect": 3, "contain": 3, "coordin": 3, "copi": 4, "copyright": 3, "corner": 3, "correct": 3, "correctli": 3, "could": 3, "countdown": 3, "cours": 0, "creat": [0, 1, 2, 3], "current": 3, "custom": 2, "cycl": 3, "dai": 3, "dashboard": 0, "date": 3, "dd": 3, "decim": 3, "default": 3, "defin": 3, "desir": 3, "desktop": 3, "detail": 0, "develop": [0, 2], "dictionari": 3, "directli": 4, "discord": 0, "displai": [0, 2], "dither": 3, "do": 3, "doesn": [0, 3], "don": 0, "donat": 0, "download": [3, 4], "draw": 3, "draw_bord": [2, 3], "drawn": 3, "driver": 3, "dry_run": 3, "e": [0, 3, 4], "each": 3, "eas": 3, "edit": 0, "effort": 0, "els": 3, "en": 3, "end": 3, "environ": 0, "epap": 3, "epaper_model": 3, "establish": 3, "etc": 0, "even": 0, "event": [0, 3], "exampl": 3, "except": 3, "exit": 3, "extract": 3, "face": 0, "fals": 3, "feed": 0, "fetch": 0, "few": 0, "file": [0, 2, 3], "filenotfounderror": 3, "filepath": 3, "fill": 3, "fill_height": 3, "fill_width": 3, "first": 3, "fit": 3, "flag": 3, "flip": 3, "fmt": 3, "folder": [3, 4], "follow": 3, "font": 3, "fontfil": 3, "fontnam": 3, "fontsiz": 3, "forecast": 0, "form": 0, "format": 3, "found": 3, "free": 0, "friendli": 0, "from": [0, 3], "full": [0, 3], "fulli": 0, "function": 2, "g": 3, "gener": [3, 4], "get": [0, 3], "get_ev": 3, "get_font": [2, 3], "get_system_tz": [2, 3], "git": 4, "github": 4, "given": 3, "go": 4, "googl": [0, 3], "gpicview": 3, "grai": 3, "greater": 3, "ha": [0, 3], "handl": 3, "have": [0, 3], "height": 3, "height_shrink_percentag": 3, "help": 0, "helper": 2, "hh": 3, "home": 3, "horizont": 3, "hour": 0, "htpp": 3, "http": [3, 4], "i": [0, 1, 3], "ical_pars": 3, "icalendar": [0, 2, 3], "idea": 0, "im_black": 3, "im_colour": 3, "imag": 3, "image1": 3, "image2": 3, "image_to_palett": [2, 3], "imagefont": 3, "imga": 3, "import": 3, "improv": 3, "increas": 3, "indefinit": 3, "index": 2, "individu": 3, "infin": 3, "info": 3, "inform": 0, "initi": 3, "inky_imag": 3, "inkyimag": [2, 3], "input": 3, "instal": 2, "instanc": 3, "instead": 3, "int": 3, "integ": 3, "internet": 3, "internet_avail": [2, 3], "interv": 3, "interval_min": 3, "invest": 0, "io": 3, "issu": 3, "iter": 3, "its": 0, "joke": 0, "json": 3, "keep": 0, "kwarg": 3, "larg": 0, "latest": [0, 3], "layout": 3, "left": 3, "lib": 3, "line": 3, "list": 3, "liter": 3, "load": 3, "load_from_fil": 3, "load_url": 3, "local": 3, "logo": 3, "long": 3, "look": [0, 3], "loop": 3, "made": 3, "mai": 0, "main": [0, 3], "mainli": [0, 1], "map": 3, "max_height": 3, "max_width": 3, "maximum": 3, "mean": 0, "merg": 3, "minut": 3, "miss": 0, "mm": 3, "mmm": 3, "mode": 3, "model": 3, "modifi": 3, "modul": [0, 1, 2, 3], "modular": 0, "monthli": 0, "more": [0, 3, 4], "moudul": 3, "much": 3, "multipl": 3, "name": 3, "navig": 4, "need": 0, "network": 3, "new": [0, 3], "next": [0, 3], "nice_p": 3, "non": [0, 3], "none": 3, "nonstop": 3, "noob": 0, "noth": 0, "now": 3, "number": 3, "object": 3, "onc": 3, "one": 3, "ones": 3, "onli": 3, "open": 0, "oper": 3, "optim": 3, "option": 3, "order": 3, "ordin": 3, "organis": 0, "oserror": 3, "other": [0, 3], "output": 3, "own": 0, "packag": 3, "page": 2, "palett": 3, "paper": [0, 3], "paramet": 3, "pars": 3, "part": 3, "parti": [0, 1], "password": 3, "past": 3, "path": 3, "path1": 3, "path2": 3, "percentag": 3, "phone": 0, "pi": [0, 3, 4], "pil": 3, "pinch": 0, "pip3": 4, "pixel": 3, "plain": 3, "pleas": [0, 4], "png": 3, "point": 3, "posit": 3, "possibl": 3, "present": 3, "preview": 3, "previous": 3, "print": 3, "process": 3, "process_modul": 3, "program": 3, "project": [0, 3], "protect": 3, "provid": 0, "py": 3, "python3": [0, 3], "quickstart": 2, "radiu": 3, "rais": 3, "rapsbian": 3, "raspberri": [0, 4], "raw": 3, "re": 0, "reach": 3, "readabl": 3, "readthedoc": 3, "rectangl": 3, "red": 3, "reduc": 3, "refresh": 3, "remain": 3, "remov": 3, "remove_alpha": 3, "render": 3, "replac": 3, "repo": 4, "repres": 3, "requir": 3, "resiz": 3, "return": 3, "rgba": 3, "right": 3, "rotat": 3, "round": 3, "rss": 0, "run": [0, 3], "run_onc": 3, "runner": 3, "sampl": 3, "save": 3, "scale": 3, "schedul": 3, "search": [2, 3], "second": 3, "see": 3, "select": [0, 3], "set": [0, 2, 3], "settings_path": 3, "sever": 0, "shade": 3, "share": 0, "should": 3, "show": [0, 3], "show_ev": 3, "shown": 3, "shrink": 3, "shrinkag": 3, "shutdown_after_run": 3, "singl": 3, "site": 3, "size": 3, "sleep": 3, "smaller": 3, "smile": 0, "softwar": 0, "solid": 3, "some": 0, "someth": [0, 3], "soon": 4, "sort": 3, "sourc": 0, "specifi": 3, "split": 3, "stai": 0, "start": 3, "static": 3, "stop": 3, "str": 3, "string": 3, "student": 0, "support": [0, 3], "sync": 0, "synchronis": 0, "system": 3, "sytax": 3, "t": [0, 3], "take": [0, 3], "test": 3, "text": 3, "text_wrap": [2, 3], "than": 3, "thank": 0, "them": [0, 3], "thi": [0, 1, 3], "thick": 3, "third": [0, 1], "time": [0, 3], "timelin": 3, "timeline_end": 3, "timeline_start": 3, "timeout": 3, "timezon": 3, "token": 3, "too": 0, "top": 3, "transpar": 3, "tri": 3, "true": 3, "truetyp": 3, "tupl": 3, "two": 3, "type": 3, "typeerror": 3, "tz": 3, "u": 0, "ui": [0, 4], "univers": 0, "until": 3, "up": 0, "updat": 3, "url": 3, "url1": 3, "url2": 3, "us": 3, "use_pi_sugar": 3, "user": 0, "usernam": 3, "usual": 3, "utc": 3, "valid": 3, "valu": 3, "valueerror": 3, "venv": 3, "veri": 3, "vertic": 3, "via": [0, 4], "wa": [0, 3], "wai": 3, "we": 0, "weather": 0, "web": [0, 4], "week": 0, "welcom": 0, "well": 0, "what": 0, "when": 3, "where": 3, "which": [0, 3], "white": 3, "who": 1, "width": 3, "width_shrink_percentag": 3, "wish": 1, "without": [0, 3], "work": [0, 3], "write": [0, 2, 3], "written": 3, "x": 3, "xy": 3, "y": 3, "yai": 0, "yellow": 3, "you": 0, "your": [0, 3, 4], "yy": 3, "zero": 0}, "titles": ["About Inkycal", "Developer documentation", "Inkycal documentation", "Inkycal", "Quickstart"], "titleterms": {"about": 0, "class": 3, "content": 2, "creat": 4, "custom": 3, "develop": 1, "displai": 3, "document": [1, 2], "file": 4, "function": 3, "helper": 3, "indic": 2, "inkyc": [0, 2, 3, 4], "instal": 4, "quickstart": 4, "set": 4, "tabl": 2}}) \ No newline at end of file +Search.setIndex({"alltitles": {"About Inkycal": [[0, null]], "Contents:": [[2, null]], "Creating settings file": [[4, "creating-settings-file"]], "Custom functions": [[3, "module-inkycal.custom.functions"]], "Developer documentation": [[1, null]], "Display": [[3, "module-inkycal.display.Display"]], "Helper classes": [[3, "module-inkycal.modules.ical_parser"]], "Indices and tables": [[2, "indices-and-tables"]], "Inkycal": [[3, null]], "Inkycal documentation": [[2, null]], "Installing Inkycal": [[4, "installing-inkycal"]], "Quickstart": [[4, null]]}, "docnames": ["about", "dev_doc", "index", "inkycal", "quickstart"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["about.md", "dev_doc.md", "index.rst", "inkycal.rst", "quickstart.md"], "indexentries": {"all_day() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.all_day", false]], "auto_fontsize() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.auto_fontsize", false]], "autoflip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.autoflip", false]], "calibrate() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.calibrate", false]], "clear() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.clear", false]], "clear_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.clear_events", false]], "countdown() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.countdown", false]], "draw_border() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.draw_border", false]], "dry_run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.dry_run", false]], "flip() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.flip", false]], "get_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_events", false]], "get_fonts() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_fonts", false]], "get_system_tz() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.get_system_tz", false]], "get_system_tz() (inkycal.modules.ical_parser.icalendar static method)": [[3, "inkycal.modules.ical_parser.iCalendar.get_system_tz", false]], "icalendar (class in inkycal.modules.ical_parser)": [[3, "inkycal.modules.ical_parser.iCalendar", false]], "image_to_palette() (in module inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.image_to_palette", false]], "inkycal (class in inkycal.main)": [[3, "inkycal.main.Inkycal", false]], "inkycal.custom.functions": [[3, "module-inkycal.custom.functions", false]], "inkycal.display.display": [[3, "module-inkycal.display.Display", false]], "inkycal.main": [[3, "module-inkycal.main", false]], "inkycal.modules.ical_parser": [[3, "module-inkycal.modules.ical_parser", false]], "inkycal.modules.inky_image": [[3, "module-inkycal.modules.inky_image", false]], "inkyimage (class in inkycal.modules.inky_image)": [[3, "inkycal.modules.inky_image.Inkyimage", false]], "internet_available() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.internet_available", false]], "load() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.load", false]], "load_from_file() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_from_file", false]], "load_url() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.load_url", false]], "merge() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.merge", false]], "module": [[3, "module-inkycal.custom.functions", false], [3, "module-inkycal.display.Display", false], [3, "module-inkycal.main", false], [3, "module-inkycal.modules.ical_parser", false], [3, "module-inkycal.modules.inky_image", false]], "preview() (inkycal.modules.inky_image.inkyimage static method)": [[3, "inkycal.modules.inky_image.Inkyimage.preview", false]], "process_module() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.process_module", false]], "remove_alpha() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.remove_alpha", false]], "resize() (inkycal.modules.inky_image.inkyimage method)": [[3, "inkycal.modules.inky_image.Inkyimage.resize", false]], "run() (inkycal.main.inkycal method)": [[3, "inkycal.main.Inkycal.run", false]], "show_events() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.show_events", false]], "sort() (inkycal.modules.ical_parser.icalendar method)": [[3, "inkycal.modules.ical_parser.iCalendar.sort", false]], "text_wrap() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.text_wrap", false]], "write() (in module inkycal.custom.functions)": [[3, "inkycal.custom.functions.write", false]]}, "objects": {"inkycal": [[3, 0, 0, "-", "main"]], "inkycal.custom": [[3, 0, 0, "-", "functions"]], "inkycal.custom.functions": [[3, 1, 1, "", "auto_fontsize"], [3, 1, 1, "", "draw_border"], [3, 1, 1, "", "get_fonts"], [3, 1, 1, "", "get_system_tz"], [3, 1, 1, "", "internet_available"], [3, 1, 1, "", "text_wrap"], [3, 1, 1, "", "write"]], "inkycal.display": [[3, 0, 0, "-", "Display"]], "inkycal.main": [[3, 2, 1, "", "Inkycal"]], "inkycal.main.Inkycal": [[3, 3, 1, "", "calibrate"], [3, 3, 1, "", "countdown"], [3, 3, 1, "", "dry_run"], [3, 3, 1, "", "process_module"], [3, 3, 1, "", "run"]], "inkycal.modules": [[3, 0, 0, "-", "ical_parser"], [3, 0, 0, "-", "inky_image"]], "inkycal.modules.ical_parser": [[3, 2, 1, "", "iCalendar"]], "inkycal.modules.ical_parser.iCalendar": [[3, 3, 1, "", "all_day"], [3, 3, 1, "", "clear_events"], [3, 3, 1, "", "get_events"], [3, 3, 1, "", "get_system_tz"], [3, 3, 1, "", "load_from_file"], [3, 3, 1, "", "load_url"], [3, 3, 1, "", "show_events"], [3, 3, 1, "", "sort"]], "inkycal.modules.inky_image": [[3, 2, 1, "", "Inkyimage"], [3, 1, 1, "", "image_to_palette"]], "inkycal.modules.inky_image.Inkyimage": [[3, 3, 1, "", "autoflip"], [3, 3, 1, "", "clear"], [3, 3, 1, "", "flip"], [3, 3, 1, "", "load"], [3, 3, 1, "", "merge"], [3, 3, 1, "", "preview"], [3, 3, 1, "", "remove_alpha"], [3, 3, 1, "", "resize"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"": [0, 3], "0": 3, "1": 3, "10": 3, "100": 3, "11": 3, "16": 3, "16grai": 3, "180": 3, "2": 3, "20": 3, "270": 3, "3": 3, "32": 3, "360": 3, "5": 3, "7": 3, "80": 3, "9": 3, "90": 3, "A": 3, "If": 3, "In": 3, "It": 0, "No": 0, "OR": 3, "The": [0, 3], "Then": 3, "To": 3, "_": 0, "about": 2, "access": 3, "accur": 3, "aceinnolab": [3, 4], "aceisac": 0, "actual": 3, "ad": 0, "add": 3, "adjust": 3, "after": 3, "agenda": 0, "align": 3, "aliv": 0, "all": [0, 3], "all_dai": 3, "allow": 3, "alpha": 3, "also": 0, "amount": 0, "an": 3, "angl": 3, "anti": 3, "anyth": 0, "arg": 3, "arrow": 3, "assembl": 3, "async": 3, "atom": 0, "attempt": 3, "attribut": 3, "auto_fonts": [2, 3], "autofit": 3, "autoflip": 3, "automat": 3, "avail": 3, "awar": 3, "band": 3, "base": 3, "befor": 3, "begin": 3, "behind": 0, "below": 3, "black": 3, "blend": 0, "bool": 3, "boot": 3, "border": 3, "box": 3, "box_siz": 3, "built": 0, "bw": 3, "bwr": 3, "bwy": 3, "calcul": 3, "calendar": 0, "calibr": 3, "can": [0, 3], "care": [0, 3], "case": 0, "caus": 3, "cd": 4, "center": 3, "chang": 3, "check": 3, "choos": 3, "chunk": 3, "class": 2, "clear": 3, "clear_ev": 3, "clockwis": 3, "clone": 4, "co": 3, "coffe": 0, "colour": 3, "com": [3, 4], "come": 4, "commerci": 0, "commonli": 3, "commun": 0, "compat": 0, "config": 3, "connect": 3, "contain": 3, "coordin": 3, "copi": 4, "copyright": 3, "corner": 3, "correct": 3, "correctli": 3, "could": 3, "countdown": 3, "cours": 0, "creat": [0, 1, 2, 3], "current": 3, "custom": 2, "cycl": 3, "dai": 3, "dashboard": 0, "date": 3, "dd": 3, "decim": 3, "default": 3, "defin": 3, "desir": 3, "desktop": 3, "detail": 0, "develop": [0, 2], "dictionari": 3, "directli": 4, "discord": 0, "displai": [0, 2], "dither": 3, "do": 3, "doesn": [0, 3], "don": 0, "donat": 0, "download": [3, 4], "draw": 3, "draw_bord": [2, 3], "drawn": 3, "driver": 3, "dry_run": 3, "e": [0, 3, 4], "each": 3, "eas": 3, "edit": 0, "effort": 0, "els": 3, "en": 3, "end": 3, "environ": 0, "epap": 3, "epaper_model": 3, "establish": 3, "etc": 0, "even": 0, "event": [0, 3], "exampl": 3, "except": 3, "exit": 3, "extract": 3, "face": 0, "fals": 3, "feed": 0, "fetch": 0, "few": 0, "file": [0, 2, 3], "filenotfounderror": 3, "filepath": 3, "fill": 3, "fill_height": 3, "fill_width": 3, "first": 3, "fit": 3, "flag": 3, "flip": 3, "fmt": 3, "folder": [3, 4], "follow": 3, "font": 3, "fontfil": 3, "fontnam": 3, "fontsiz": 3, "forecast": 0, "form": 0, "format": 3, "found": 3, "free": 0, "friendli": 0, "from": [0, 3], "full": [0, 3], "fulli": 0, "function": 2, "g": 3, "gener": [3, 4], "get": [0, 3], "get_ev": 3, "get_font": [2, 3], "get_system_tz": [2, 3], "git": 4, "github": 4, "given": 3, "go": 4, "googl": [0, 3], "gpicview": 3, "grai": 3, "greater": 3, "ha": [0, 3], "handl": 3, "have": [0, 3], "height": 3, "height_shrink_percentag": 3, "help": 0, "helper": 2, "hh": 3, "home": 3, "horizont": 3, "hour": 0, "htpp": 3, "http": [3, 4], "i": [0, 1, 3], "ical_pars": 3, "icalendar": [0, 2, 3], "idea": 0, "im_black": 3, "im_colour": 3, "imag": 3, "image1": 3, "image2": 3, "image_to_palett": [2, 3], "imagefont": 3, "imga": 3, "import": 3, "improv": 3, "increas": 3, "indefinit": 3, "index": 2, "individu": 3, "infin": 3, "info": 3, "inform": 0, "initi": 3, "inky_imag": 3, "inkyimag": [2, 3], "input": 3, "instal": 2, "instanc": 3, "instead": 3, "int": 3, "integ": 3, "internet": 3, "internet_avail": [2, 3], "interv": 3, "interval_min": 3, "invest": 0, "io": 3, "issu": 3, "iter": 3, "its": 0, "joke": 0, "json": 3, "keep": 0, "kwarg": 3, "larg": 0, "latest": [0, 3], "layout": 3, "left": 3, "lib": 3, "line": 3, "list": 3, "liter": 3, "load": 3, "load_from_fil": 3, "load_url": 3, "local": 3, "logo": 3, "long": 3, "look": [0, 3], "loop": 3, "made": 3, "mai": 0, "main": [0, 3], "mainli": [0, 1], "map": 3, "max_height": 3, "max_width": 3, "maximum": 3, "mean": 0, "merg": 3, "minut": 3, "miss": 0, "mm": 3, "mmm": 3, "mode": 3, "model": 3, "modifi": 3, "modul": [0, 1, 2, 3], "modular": 0, "monthli": 0, "more": [0, 3, 4], "moudul": 3, "much": 3, "multipl": 3, "name": 3, "navig": 4, "need": 0, "network": 3, "new": [0, 3], "next": [0, 3], "nice_p": 3, "non": [0, 3], "none": 3, "nonstop": 3, "noob": 0, "noth": 0, "now": 3, "number": 3, "object": 3, "onc": 3, "one": 3, "ones": 3, "onli": 3, "open": 0, "oper": 3, "optim": 3, "option": 3, "order": 3, "ordin": 3, "organis": 0, "oserror": 3, "other": [0, 3], "output": 3, "own": 0, "packag": 3, "page": 2, "palett": 3, "paper": [0, 3], "paramet": 3, "pars": 3, "part": 3, "parti": [0, 1], "password": 3, "past": 3, "path": 3, "path1": 3, "path2": 3, "percentag": 3, "phone": 0, "pi": [0, 3, 4], "pil": 3, "pinch": 0, "pip3": 4, "pixel": 3, "plain": 3, "pleas": [0, 4], "png": 3, "point": 3, "posit": 3, "possibl": 3, "present": 3, "preview": 3, "previous": 3, "print": 3, "process": 3, "process_modul": 3, "program": 3, "project": [0, 3], "protect": 3, "provid": 0, "py": 3, "python3": [0, 3], "quickstart": 2, "radiu": 3, "rais": 3, "rapsbian": 3, "raspberri": [0, 4], "raw": 3, "re": 0, "reach": 3, "readabl": 3, "readthedoc": 3, "rectangl": 3, "red": 3, "reduc": 3, "refresh": 3, "remain": 3, "remov": 3, "remove_alpha": 3, "render": 3, "replac": 3, "repo": 4, "repres": 3, "requir": 3, "resiz": 3, "return": 3, "rgba": 3, "right": 3, "rotat": 3, "round": 3, "rss": 0, "run": [0, 3], "run_onc": 3, "runner": 3, "sampl": 3, "save": 3, "scale": 3, "schedul": 3, "search": [2, 3], "second": 3, "see": 3, "select": [0, 3], "set": [0, 2, 3], "settings_path": 3, "sever": 0, "shade": 3, "share": 0, "should": 3, "show": [0, 3], "show_ev": 3, "shown": 3, "shrink": 3, "shrinkag": 3, "shutdown_after_run": 3, "singl": 3, "site": 3, "size": 3, "sleep": 3, "smaller": 3, "smile": 0, "softwar": 0, "solid": 3, "some": 0, "someth": [0, 3], "soon": 4, "sort": 3, "sourc": 0, "specifi": 3, "split": 3, "stai": 0, "start": 3, "static": 3, "stop": 3, "str": 3, "string": 3, "student": 0, "support": [0, 3], "sync": 0, "synchronis": 0, "system": 3, "sytax": 3, "t": [0, 3], "take": [0, 3], "test": 3, "text": 3, "text_wrap": [2, 3], "than": 3, "thank": 0, "them": [0, 3], "thi": [0, 1, 3], "thick": 3, "third": [0, 1], "time": [0, 3], "timelin": 3, "timeline_end": 3, "timeline_start": 3, "timeout": 3, "timezon": 3, "token": 3, "too": 0, "top": 3, "transpar": 3, "tri": 3, "true": 3, "truetyp": 3, "tupl": 3, "two": 3, "type": 3, "typeerror": 3, "tz": 3, "u": 0, "ui": [0, 4], "univers": 0, "until": 3, "up": 0, "updat": 3, "url": 3, "url1": 3, "url2": 3, "us": 3, "use_pi_sugar": 3, "user": 0, "usernam": 3, "usual": 3, "utc": 3, "valid": 3, "valu": 3, "valueerror": 3, "venv": 3, "veri": 3, "vertic": 3, "via": [0, 4], "wa": [0, 3], "wai": 3, "we": 0, "weather": 0, "web": [0, 4], "week": 0, "welcom": 0, "well": 0, "what": 0, "when": 3, "where": 3, "which": [0, 3], "white": 3, "who": 1, "width": 3, "width_shrink_percentag": 3, "wish": 1, "without": [0, 3], "work": [0, 3], "write": [0, 2, 3], "written": 3, "x": 3, "xy": 3, "y": 3, "yai": 0, "yellow": 3, "you": 0, "your": [0, 3, 4], "yy": 3, "zero": 0}, "titles": ["About Inkycal", "Developer documentation", "Inkycal documentation", "Inkycal", "Quickstart"], "titleterms": {"about": 0, "class": 3, "content": 2, "creat": 4, "custom": 3, "develop": 1, "displai": 3, "document": [1, 2], "file": 4, "function": 3, "helper": 3, "indic": 2, "inkyc": [0, 2, 3, 4], "instal": 4, "quickstart": 4, "set": 4, "tabl": 2}}) \ No newline at end of file From d2884df7e21165ab5d378cf471f414863f38fac1 Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 22 Jul 2024 10:53:23 +0200 Subject: [PATCH 61/62] switch to older release --- .github/workflows/update-os.yml | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index 51c4f91..c097194 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -24,7 +24,8 @@ jobs: TINDIE_USERNAME: ${{ secrets.TINDIE_USERNAME }} with: # Set the base_image to the desired Raspberry Pi OS version - base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/2023-12-11-raspios-bookworm-armhf-lite.img.xz + # note: version 2023-12-11 seems to have issues with the kernel and gpio + base_image: https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2023-12-06/2023-12-05-raspios-bookworm-arm64-lite.img.xz image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | diff --git a/README.md b/README.md index e465de6..b205abe 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ display!** ## Configuring the Raspberry Pi -Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-12-11/2023-12-11-raspios-bookworm-armhf-lite.img.xz) as the latest release is known to have some issues with the latest kernel update. +Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2023-12-06/2023-12-05-raspios-bookworm-arm64-lite.img.xz) as the latest release is known to have some issues with the latest kernel update. | option | value | |:--------------------------|:---------------------------:| From 5fdcd5f1a1e47b7af72d5af22cb9f7a29b702529 Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 23 Jul 2024 01:31:39 +0200 Subject: [PATCH 62/62] fix typo --- .github/workflows/update-os.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-os.yml b/.github/workflows/update-os.yml index c097194..9641a76 100644 --- a/.github/workflows/update-os.yml +++ b/.github/workflows/update-os.yml @@ -25,7 +25,7 @@ jobs: with: # Set the base_image to the desired Raspberry Pi OS version # note: version 2023-12-11 seems to have issues with the kernel and gpio - base_image: https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2023-12-06/2023-12-05-raspios-bookworm-arm64-lite.img.xz + base_image: https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz image_additional_mb: 3072 # enlarge free space to 3 GB optimize_image: true commands: | diff --git a/README.md b/README.md index b205abe..20a9dd2 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ display!** ## Configuring the Raspberry Pi -Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2023-12-06/2023-12-05-raspios-bookworm-arm64-lite.img.xz) as the latest release is known to have some issues with the latest kernel update. +Flash Raspberry Pi OS on your microSD card (min. 4GB) with [Raspberry Pi Imager](https://rptl.io/imager). Please use this version of [Raspberry Pi OS - bookworm](https://downloads.raspberrypi.com/raspios_lite_armhf/images/raspios_lite_armhf-2023-05-03/2023-05-03-raspios-bullseye-armhf-lite.img.xz) as the latest release is known to have some issues with the latest kernel update. | option | value | |:--------------------------|:---------------------------:|