@@ -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!")
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB  | 
| 
		 Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB  | 
| 
		 Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB  | 
| 
		 Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB  | 
| 
		 Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB  | 
| 
		 Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										41
									
								
								inky_run.py
									
									
									
									
									
								
							
							
						
						@@ -1,7 +1,40 @@
 | 
			
		||||
"""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
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    asyncio.run(run())
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
# Main file
 | 
			
		||||
from inkycal.display import Display
 | 
			
		||||
from inkycal.main import Inkycal
 | 
			
		||||
import inkycal.modules.inkycal_stocks
 | 
			
		||||
 
 | 
			
		||||
@@ -17,20 +17,16 @@ from PIL import Image
 | 
			
		||||
from PIL import ImageDraw
 | 
			
		||||
from PIL import ImageFont
 | 
			
		||||
 | 
			
		||||
from inkycal.settings import Settings
 | 
			
		||||
 | 
			
		||||
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])
 | 
			
		||||
 | 
			
		||||
# 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]
 | 
			
		||||
 
 | 
			
		||||
@@ -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 list(supported_models.keys())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								inkycal/loggers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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.INFO)
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
							
								
								
									
										311
									
								
								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 import JSONCache
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
settings = Settings()
 | 
			
		||||
 | 
			
		||||
CACHE_NAME = "inkycal_main"
 | 
			
		||||
 | 
			
		||||
# TODO: autostart -> supervisor?
 | 
			
		||||
 | 
			
		||||
class Inkycal:
 | 
			
		||||
    """Inkycal main class
 | 
			
		||||
@@ -60,43 +37,45 @@ 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"
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
        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:
 | 
			
		||||
                    settings = json.load(settings_file)
 | 
			
		||||
                    self.settings = settings
 | 
			
		||||
                    self.settings = json.load(settings_file)
 | 
			
		||||
 | 
			
		||||
            except FileNotFoundError:
 | 
			
		||||
                raise FileNotFoundError(
 | 
			
		||||
                    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') 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 self.disable_calibration:
 | 
			
		||||
            logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.")
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
@@ -109,10 +88,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']
 | 
			
		||||
@@ -122,7 +101,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 +110,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,57 +125,83 @@ 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)
 | 
			
		||||
 | 
			
		||||
        # Give an OK message
 | 
			
		||||
        print('loaded inkycal')
 | 
			
		||||
        # 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()
 | 
			
		||||
 | 
			
		||||
    def countdown(self, interval_mins: int or None = None) -> int:
 | 
			
		||||
        """Returns the remaining time in seconds until next display update.
 | 
			
		||||
        self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"])
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
            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
 | 
			
		||||
        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.
 | 
			
		||||
 | 
			
		||||
        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_to_next_interval
 | 
			
		||||
 | 
			
		||||
            # Return seconds until next update
 | 
			
		||||
            return remaining_time
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@@ -206,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
 | 
			
		||||
@@ -218,20 +220,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")
 | 
			
		||||
                print("OK!")
 | 
			
		||||
            except Exception:
 | 
			
		||||
            success = self.process_module(number)
 | 
			
		||||
            if success:
 | 
			
		||||
                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!  "
 | 
			
		||||
                logger.exception("Error!")
 | 
			
		||||
                logger.exception(f"Exception: {traceback.format_exc()}.")
 | 
			
		||||
 | 
			
		||||
        if errors:
 | 
			
		||||
            logger.error('Error/s in modules:', *errors)
 | 
			
		||||
@@ -277,76 +272,69 @@ 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()
 | 
			
		||||
 | 
			
		||||
        # 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"]}')
 | 
			
		||||
        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
 | 
			
		||||
            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()}.")
 | 
			
		||||
                    self.info += f"im {number}: X  "
 | 
			
		||||
 | 
			
		||||
            if errors:
 | 
			
		||||
                logger.error("Error/s in modules:", *errors)
 | 
			
		||||
                counter = 0
 | 
			
		||||
                self.counter = 0
 | 
			
		||||
                self.cache_data["counter"] = 0
 | 
			
		||||
            else:
 | 
			
		||||
                counter += 1
 | 
			
		||||
                logger.info("successful")
 | 
			
		||||
                self.counter += 1
 | 
			
		||||
                self.cache_data["counter"] += 1
 | 
			
		||||
                logger.info("All images generated successfully!")
 | 
			
		||||
            del errors
 | 
			
		||||
 | 
			
		||||
            if self.battery_capacity < 20:
 | 
			
		||||
                self.info += "Low battery!  "
 | 
			
		||||
 | 
			
		||||
            # Assemble image from each module - add info section if specified
 | 
			
		||||
            self._assemble()
 | 
			
		||||
 | 
			
		||||
            # 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:
 | 
			
		||||
                    # 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 +346,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:
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    im_black = self._merge_bands()
 | 
			
		||||
 | 
			
		||||
                    # Flip the image by 180° if required
 | 
			
		||||
@@ -376,14 +362,29 @@ 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()}')
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
            # Exit the loop if run_once is True
 | 
			
		||||
            if run_once:
 | 
			
		||||
                break  # Exit the loop after one full cycle if run_once is True
 | 
			
		||||
 | 
			
		||||
            sleep_time = self.countdown()
 | 
			
		||||
 | 
			
		||||
            if self.use_pi_sugar:
 | 
			
		||||
                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:
 | 
			
		||||
                    logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}")
 | 
			
		||||
                else:
 | 
			
		||||
                    logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}")
 | 
			
		||||
 | 
			
		||||
            await asyncio.sleep(sleep_time)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -392,7 +393,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 +533,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 +576,40 @@ 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
 | 
			
		||||
 | 
			
		||||
    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')
 | 
			
		||||
 
 | 
			
		||||
@@ -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)     #
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -80,7 +78,7 @@ class Agenda(inkycal_module):
 | 
			
		||||
        self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize)
 | 
			
		||||
 | 
			
		||||
        # give an OK message
 | 
			
		||||
        print(f'{__name__} loaded')
 | 
			
		||||
        logger.debug(f'{__name__} loaded')
 | 
			
		||||
 | 
			
		||||
    def generate_image(self):
 | 
			
		||||
        """Generate image for this module"""
 | 
			
		||||
@@ -90,7 +88,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')
 | 
			
		||||
 
 | 
			
		||||
@@ -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')
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -322,7 +322,7 @@ class Calendar(inkycal_module):
 | 
			
		||||
                        im_colour,
 | 
			
		||||
                        grid[days],
 | 
			
		||||
                        (icon_width, icon_height),
 | 
			
		||||
                        radius=6,
 | 
			
		||||
                        radius=6
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
            # Filter upcoming events until 4 weeks in the future
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -237,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):
 | 
			
		||||
        """
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
 
 | 
			
		||||
@@ -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 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,19 +64,20 @@ 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
 | 
			
		||||
 | 
			
		||||
        # give an OK message
 | 
			
		||||
        print(f'{__name__} loaded')
 | 
			
		||||
        logger.debug(f'{__name__} loaded')
 | 
			
		||||
 | 
			
		||||
    def generate_image(self):
 | 
			
		||||
        """Generate image for this module"""
 | 
			
		||||
@@ -86,17 +87,19 @@ 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(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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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')
 | 
			
		||||
@@ -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])
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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')
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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"""
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -143,7 +143,7 @@ class Weather(inkycal_module):
 | 
			
		||||
            self.tempDispUnit = "°"
 | 
			
		||||
 | 
			
		||||
        # give an OK message
 | 
			
		||||
        print(f"{__name__} loaded")
 | 
			
		||||
        logger.debug(f"{__name__} loaded")
 | 
			
		||||
 | 
			
		||||
    def generate_image(self):
 | 
			
		||||
        """Generate image for this module"""
 | 
			
		||||
@@ -152,7 +152,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')
 | 
			
		||||
@@ -160,8 +160,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():
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ class Webshot(inkycal_module):
 | 
			
		||||
                raise Exception("Rotation must be either 0, 90, 180 or 270")
 | 
			
		||||
 | 
			
		||||
        # give an OK message
 | 
			
		||||
        print(f'Inkycal webshot loaded')
 | 
			
		||||
        logger.debug(f'Inkycal webshot loaded')
 | 
			
		||||
 | 
			
		||||
    def generate_image(self):
 | 
			
		||||
        """Generate image for this module"""
 | 
			
		||||
@@ -99,7 +99,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')
 | 
			
		||||
@@ -109,6 +109,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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
@@ -51,13 +53,13 @@ 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"""
 | 
			
		||||
 | 
			
		||||
        # Create tmp path
 | 
			
		||||
        tmpPath = f"{top_level}/temp"
 | 
			
		||||
        tmpPath = settings.TEMPORARY_FOLDER
 | 
			
		||||
 | 
			
		||||
        if not os.path.exists(tmpPath):
 | 
			
		||||
            os.mkdir(tmpPath)
 | 
			
		||||
@@ -66,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')
 | 
			
		||||
@@ -76,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								inkycal/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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"
 | 
			
		||||
							
								
								
									
										2
									
								
								inkycal/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
from .pisugar import PiSugar
 | 
			
		||||
from .json_cache import JSONCache
 | 
			
		||||
							
								
								
									
										28
									
								
								inkycal/utils/json_cache.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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)
 | 
			
		||||
							
								
								
									
										147
									
								
								inkycal/utils/pisugar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,147 @@
 | 
			
		||||
"""PiSugar helper class for Inkycal."""
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
from inkycal.settings import Settings
 | 
			
		||||
import arrow
 | 
			
		||||
 | 
			
		||||
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", "rtc_alarm_set"]
 | 
			
		||||
 | 
			
		||||
    def _get_output(self, command, param=None):
 | 
			
		||||
        if command not in self.allowed_commands:
 | 
			
		||||
            logger.error(f"Command {command} not allowed")
 | 
			
		||||
            return None
 | 
			
		||||
        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:
 | 
			
		||||
                print(f"Command failed with {result.stderr}")
 | 
			
		||||
                return None
 | 
			
		||||
            output = result.stdout.strip()
 | 
			
		||||
            return output
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Error executing command: {e}")
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def get_battery(self) -> float 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 float(line.split(':')[1].strip())
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_model(self) -> str or None:
 | 
			
		||||
        """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) -> arrow.arrow or None:
 | 
			
		||||
        """Get the RTC time."""
 | 
			
		||||
        result = self._get_output("get rtc_time")
 | 
			
		||||
        if result:
 | 
			
		||||
            rtc_time = result.split("rtc_time: ")[1].strip()
 | 
			
		||||
            return arrow.get(rtc_time)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_rtc_alarm_enabled(self) -> str or None:
 | 
			
		||||
        """Get the RTC alarm enabled status."""
 | 
			
		||||
        result = self._get_output("get rtc_alarm_enabled")
 | 
			
		||||
        if result:
 | 
			
		||||
            second_line = result.splitlines()[1]
 | 
			
		||||
            output = second_line.split('rtc_alarm_enabled: ')[1].strip()
 | 
			
		||||
            return True if output == "true" else False
 | 
			
		||||
        return 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:
 | 
			
		||||
            alarm_time = result.split('rtc_alarm_time: ')[1].strip()
 | 
			
		||||
            return arrow.get(alarm_time)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_alarm_repeat(self) -> dict or None:
 | 
			
		||||
        """Get the alarm repeat status.
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
            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":
 | 
			
		||||
                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:
 | 
			
		||||
            status = result.split('rtc_pi2rtc: ')[1].strip()
 | 
			
		||||
            if status == "done":
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    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", f"{iso_format } {repeat}")
 | 
			
		||||
        if result:
 | 
			
		||||
            status = result.split('rtc_alarm_set: ')[1].strip()
 | 
			
		||||
            if status == "done":
 | 
			
		||||
                return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ tests = [
 | 
			
		||||
            "padding_x": 10,
 | 
			
		||||
            "padding_y": 10,
 | 
			
		||||
            "fontsize": 12,
 | 
			
		||||
            "language": "en"
 | 
			
		||||
            "language": "de"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -63,6 +68,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')
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||