checkpoint

This commit is contained in:
Ace 2024-05-12 02:00:26 +02:00
parent 9346fcf750
commit 610f246c02
18 changed files with 225 additions and 180 deletions

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,20 +1,15 @@
# Display class (for driving E-Paper displays)
from inkycal.display import Display
# Default modules # Default modules
import inkycal.modules.inkycal_agenda import inkycal.modules.inkycal_agenda
import inkycal.modules.inkycal_calendar import inkycal.modules.inkycal_calendar
import inkycal.modules.inkycal_weather
import inkycal.modules.inkycal_feeds import inkycal.modules.inkycal_feeds
import inkycal.modules.inkycal_todoist import inkycal.modules.inkycal_fullweather
import inkycal.modules.inkycal_image import inkycal.modules.inkycal_image
import inkycal.modules.inkycal_jokes import inkycal.modules.inkycal_jokes
import inkycal.modules.inkycal_slideshow import inkycal.modules.inkycal_slideshow
import inkycal.modules.inkycal_stocks import inkycal.modules.inkycal_stocks
import inkycal.modules.inkycal_todoist
import inkycal.modules.inkycal_weather
import inkycal.modules.inkycal_webshot import inkycal.modules.inkycal_webshot
import inkycal.modules.inkycal_xkcd import inkycal.modules.inkycal_xkcd
import inkycal.modules.inkycal_fullweather from inkycal.display import Display
import inkycal.modules.inkycal_mawaqit
# Main file
from inkycal.main import Inkycal from inkycal.main import Inkycal

View File

@ -17,20 +17,16 @@ from PIL import Image
from PIL import ImageDraw from PIL import ImageDraw
from PIL import ImageFont from PIL import ImageFont
logs = logging.getLogger(__name__) from inkycal.settings import Settings
logs.setLevel(level=logging.INFO)
# Get the path to the Inkycal folder logger = logging.getLogger(__name__)
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 settings = Settings()
fonts_location = os.path.join(top_level, "fonts/")
image_folder = os.path.join(top_level, "image_folder/")
# Get available fonts within fonts folder # Get available fonts within fonts folder
fonts = {} fonts = {}
for path, dirs, files in os.walk(fonts_location): for path, dirs, files in os.walk(settings.FONT_PATH):
for _ in files: for _ in files:
if _.endswith(".otf"): if _.endswith(".otf"):
name = _.split(".otf")[0] name = _.split(".otf")[0]
@ -39,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location):
if _.endswith(".ttf"): if _.endswith(".ttf"):
name = _.split(".ttf")[0] name = _.split(".ttf")[0]
fonts[name] = os.path.join(path, _) 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()] available_fonts = [key for key, values in fonts.items()]
@ -81,12 +77,12 @@ def get_system_tz() -> str:
""" """
try: try:
local_tz = tzlocal.get_localzone().key 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: except:
logs.error("System timezone could not be parsed!") logger.error("System timezone could not be parsed!")
logs.error("Please set timezone manually!. Falling back to UTC...") logger.error("Please set timezone manually!. Falling back to UTC...")
local_tz = "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 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 # Truncate text if text is too long, so it can fit inside the box
if (text_width, text_height) > (box_width, box_height): 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): while (text_width, text_height) > (box_width, box_height):
text = text[0:-1] text = text[0:-1]
text_bbox = font.getbbox(text) text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0] text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg") text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1] text_height = text_bbox_height[3] - text_bbox_height[1]
logs.debug(text) logger.debug(text)
# Align text to desired position # Align text to desired position
if alignment == "center" or None: if alignment == "center" or None:

View File

@ -2,22 +2,18 @@
10.3" driver class 10.3" driver class
Copyright by aceinnolab Copyright by aceinnolab
""" """
import os
from subprocess import run from subprocess import run
from PIL import Image from PIL import Image
from inkycal.custom import image_folder, top_level from inkycal.settings import Settings
# Display resolution # Display resolution
EPD_WIDTH = 1872 EPD_WIDTH = 1872
EPD_HEIGHT = 1404 EPD_HEIGHT = 1404
# Please insert VCOM of your display. The Minus sign before is not required settings = Settings()
VCOM = "2.0"
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
class EPD: class EPD:
@ -40,8 +36,8 @@ class EPD:
def getbuffer(self, image): def getbuffer(self, image):
"""ad-hoc""" """ad-hoc"""
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT) image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP')
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"}'
print(command) print(command)
return command return command

View File

@ -2,20 +2,16 @@
7.8" parallel driver class 7.8" parallel driver class
Copyright by aceinnolab Copyright by aceinnolab
""" """
import os
from subprocess import run from subprocess import run
from inkycal.custom import image_folder, top_level from inkycal.settings import Settings
# Display resolution # Display resolution
EPD_WIDTH = 1872 EPD_WIDTH = 1872
EPD_HEIGHT = 1404 EPD_HEIGHT = 1404
# Please insert VCOM of your display. The Minus sign before is not required settings = Settings()
VCOM = "2.0"
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
class EPD: class EPD:
@ -38,8 +34,8 @@ class EPD:
def getbuffer(self, image): def getbuffer(self, image):
"""ad-hoc""" """ad-hoc"""
image = image.rotate(90, expand=True) image = image.rotate(90, expand=True)
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP')
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"}'
print(command) print(command)
return command return command

View File

@ -4,18 +4,16 @@ Copyright by aceinnolab
""" """
from subprocess import run from subprocess import run
from inkycal.custom import image_folder, top_level from inkycal.settings import Settings
# Display resolution # Display resolution
EPD_WIDTH = 1200 EPD_WIDTH = 1200
EPD_HEIGHT = 825 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: class EPD:
@ -38,8 +36,8 @@ class EPD:
def getbuffer(self, image): def getbuffer(self, image):
"""ad-hoc""" """ad-hoc"""
image = image.rotate(90, expand=True) image = image.rotate(90, expand=True)
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP') image.convert('RGB').save(settings.IMAGE_FOLDER + 'canvas.bmp', 'BMP')
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"}'
print(command) print(command)
return command return command

35
inkycal/loggers.py Normal file
View 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.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)

View File

@ -6,44 +6,21 @@ Copyright by aceinnolab
import asyncio import asyncio
import glob import glob
import hashlib import hashlib
from logging.handlers import RotatingFileHandler
import numpy import numpy
from inkycal import loggers # noqa
from inkycal.custom import * from inkycal.custom import *
from inkycal.display import Display from inkycal.display import Display
from inkycal.modules.inky_image import Inkyimage as Images from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.utils.json_cache import JSONCache
# 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)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
settings = Settings()
CACHE_NAME = "inkycal_main"
# TODO: autostart -> supervisor?
class Inkycal: class Inkycal:
"""Inkycal main class """Inkycal main class
@ -62,13 +39,7 @@ class Inkycal:
def __init__(self, settings_path: str or None = None, render: bool = True): def __init__(self, settings_path: str or None = None, render: bool = True):
"""Initialise Inkycal""" """Initialise Inkycal"""
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
self.render = render self.render = render
self.info = None self.info = None
@ -77,8 +48,7 @@ class Inkycal:
if settings_path: if settings_path:
try: try:
with open(settings_path) as settings_file: with open(settings_path) as settings_file:
settings = json.load(settings_file) self.settings = json.load(settings_file)
self.settings = settings
except FileNotFoundError: except FileNotFoundError:
raise FileNotFoundError( raise FileNotFoundError(
@ -86,17 +56,19 @@ class Inkycal:
else: else:
try: try:
with open('/boot/settings.json') as settings_file: with open('/boot/settings.json', mode="r") as settings_file:
settings = json.load(settings_file) self.settings = json.load(settings_file)
self.settings = settings
except FileNotFoundError: except FileNotFoundError:
raise SettingsFileNotFoundError raise SettingsFileNotFoundError
self.disable_calibration = self.settings.get('disable_calibration', False) self.disable_calibration = self.settings.get('disable_calibration', False)
if not os.path.exists(image_folder): if not os.path.exists(settings.IMAGE_FOLDER):
os.mkdir(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 # Option to use epaper image optimisation, reduces colours
self.optimize = True self.optimize = True
@ -122,7 +94,7 @@ class Inkycal:
# Load and initialise modules specified in the settings file # Load and initialise modules specified in the settings file
self._module_number = 1 self._module_number = 1
for module in settings['modules']: for module in self.settings['modules']:
module_name = module['name'] module_name = module['name']
try: try:
loader = f'from inkycal.modules import {module_name}' loader = f'from inkycal.modules import {module_name}'
@ -131,10 +103,9 @@ class Inkycal:
setup = f'self.module_{self._module_number} = {module_name}({module})' setup = f'self.module_{self._module_number} = {module_name}({module})'
# print(setup) # print(setup)
exec(setup) exec(setup)
logger.info(('name : {name} size : {width}x{height} px'.format( width = module['config']['size'][0]
name=module_name, height = module['config']['size'][1]
width=module['config']['size'][0], logger.info(f'name : {module_name} size : {width}x{height} px')
height=module['config']['size'][1])))
self._module_number += 1 self._module_number += 1
@ -147,55 +118,56 @@ class Inkycal:
logger.exception(f"Exception: {traceback.format_exc()}.") logger.exception(f"Exception: {traceback.format_exc()}.")
# Path to store images # Path to store images
self.image_folder = image_folder self.image_folder = settings.IMAGE_FOLDER
# Remove old hashes # Remove old hashes
self._remove_hashes(self.image_folder) 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 # Give an OK message
print('loaded inkycal') print('loaded inkycal')
def countdown(self, interval_mins: int or None = None) -> int: def countdown(self, 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: Args:
- interval_mins = int -> the interval in minutes for the update interval_mins (int): The interval in minutes for the update. If none is given, the value
if no interval is given, the value from the settings file is used. from the settings file is used.
Returns: Returns:
- int -> the remaining time in seconds until next update int: The remaining time in seconds until the next update.
""" """
# Default to settings if no interval is provided
# Check if empty, if empty, use value from settings file
if interval_mins is None: if interval_mins is None:
interval_mins = self.settings["update_interval"] interval_mins = self.settings["update_interval"]
# Find out at which minutes the update should happen # Get the current time
now = arrow.now() 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 # Calculate the next update time
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute # 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 # Finding the next interval point
print(f'{minutes} minutes left until next refresh') 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 # Logging the remaining time in appropriate units
remaining_time = minutes * 60 + (60 - now.second) hours_to_next_interval = minutes_to_next_interval // 60
remaining_minutes = minutes_to_next_interval % 60
# Return seconds until next update if hours_to_next_interval > 0:
return remaining_time print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh')
else: else:
# Calculate time in minutes until next update using the range of 24 hours in steps of every full hour print(f'{remaining_minutes} minutes left until next refresh')
update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1]
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
remaining_time = minutes * 60 + (60 - now.second)
print(f'{round(minutes / 60, 1)} hours left until next refresh') return seconds_to_next_interval
# Return seconds until next update
return remaining_time
def test(self): def test(self):
"""Tests if Inkycal can run without issues. """Tests if Inkycal can run without issues.
@ -218,20 +190,13 @@ class Inkycal:
for number in range(1, self._module_number): for number in range(1, self._module_number):
name = eval(f"self.module_{number}.name") name = eval(f"self.module_{number}.name")
module = eval(f'self.module_{number}')
print(f'generating image(s) for {name}...', end="") print(f'generating image(s) for {name}...', end="")
try: success = self.process_module(number)
black, colour = module.generate_image() if success:
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!") print("OK!")
except Exception: else:
errors.append(number) errors.append(number)
self.info += f"module {number}: Error! " self.info += f"module {number}: Error! "
logger.exception("Error!")
logger.exception(f"Exception: {traceback.format_exc()}.")
if errors: if errors:
logger.error('Error/s in modules:', *errors) logger.error('Error/s in modules:', *errors)
@ -277,14 +242,17 @@ class Inkycal:
print("Refresh needed: {a}".format(a=res)) print("Refresh needed: {a}".format(a=res))
return res return res
async def run(self): async def run(self, run_once=False):
"""Runs main program in nonstop mode. """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 Args:
from all modules, assembles them in one image, refreshed the E-Paper and run_once (bool): If True, runs the updating process once and stops. If False,
then sleeps until the next scheduled update. 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 # Get the time of initial run
runtime = arrow.now() runtime = arrow.now()
@ -303,31 +271,19 @@ class Inkycal:
f"Time: {current_time.format('HH:mm')}") f"Time: {current_time.format('HH:mm')}")
print('Generating images for all modules...', end='') 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): if not self.settings.get('image_hash', False):
self.info = f"{current_time.format('D MMM @ HH:mm')} " self.info = f"{current_time.format('D MMM @ HH:mm')} "
else: else:
self.info = "" self.info = ""
for number in range(1, self._module_number): for number in range(1, self._module_number):
success = self.process_module(number)
# name = eval(f"self.module_{number}.name") if not success:
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:
errors.append(number) errors.append(number)
self.info += f"module {number}: Error! " self.info += f"module {number}: Error! "
logger.exception("Error!")
logger.exception(f"Exception: {traceback.format_exc()}.")
if errors: if errors:
logger.error("Error/s in modules:", *errors) logger.error("Error/s in modules:", *errors)
@ -343,10 +299,9 @@ class Inkycal:
# Check if image should be rendered # Check if image should be rendered
if self.render: if self.render:
display = self.Display display = self.Display
self._calibration_check() self._calibration_check()
if self._calibration_state: 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) self._remove_hashes(self.image_folder)
if self.supports_colour: if self.supports_colour:
@ -358,17 +313,15 @@ class Inkycal:
im_black = upside_down(im_black) im_black = upside_down(im_black)
im_colour = upside_down(im_colour) 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([ if not self.settings.get('image_hash', False) or self._needs_image_update([
(f"{self.image_folder}/canvas.png.hash", im_black), (f"{self.image_folder}/canvas.png.hash", im_black),
(f"{self.image_folder}/canvas_colour.png.hash", im_colour) (f"{self.image_folder}/canvas_colour.png.hash", im_colour)
]): ]):
# render the image on the display
display.render(im_black, im_colour) display.render(im_black, im_colour)
# Part for black-white ePapers # Part for black-white ePapers
elif not self.supports_colour: elif not self.supports_colour:
im_black = self._merge_bands() im_black = self._merge_bands()
# Flip the image by 180° if required # Flip the image by 180° if required
@ -376,13 +329,15 @@ class Inkycal:
im_black = upside_down(im_black) im_black = upside_down(im_black)
if not self.settings.get('image_hash', False) or self._needs_image_update([ if not self.settings.get('image_hash', False) or self._needs_image_update([
(f"{self.image_folder}/canvas.png.hash", im_black), (f"{self.image_folder}/canvas.png.hash", im_black),]):
]):
display.render(im_black) display.render(im_black)
print(f'\nNo errors since {counter} display updates \n' print(f'\nNo errors since {counter} display updates \n'
f'program started {runtime.humanize()}') 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() sleep_time = self.countdown()
await asyncio.sleep(sleep_time) await asyncio.sleep(sleep_time)
@ -392,7 +347,8 @@ class Inkycal:
returns the merged image 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 there is an image for black and colour, merge them
if os.path.exists(im1_path) and os.path.exists(im2_path): 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 = black_to_colour(im_colour)
im_colour.paste(im_black, (0, 0), im_black) 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 @staticmethod
def _optimize_im(image, threshold=220): def _optimize_im(image, threshold=220):
@ -574,13 +530,29 @@ class Inkycal:
@staticmethod @staticmethod
def cleanup(): def cleanup():
# clean up old images in image_folder # 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: try:
os.remove(_file) os.remove(_file)
except: except:
logger.error(f"could not remove file: {_file}") logger.error(f"could not remove file: {_file}")
pass 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__': if __name__ == '__main__':
print(f'running inkycal main in standalone/debug mode') print(f'running inkycal main in standalone/debug mode')

View File

@ -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 fonts
from inkycal.custom.functions import get_system_tz from inkycal.custom.functions import get_system_tz
from inkycal.custom.functions import internet_available from inkycal.custom.functions import internet_available
from inkycal.custom.functions import top_level
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
from inkycal.modules.inky_image import image_to_palette from inkycal.modules.inky_image import image_to_palette
from inkycal.modules.template import inkycal_module from inkycal.modules.template import inkycal_module
from inkycal.settings import Settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) 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: def outline(image: Image, size: int, color: tuple) -> Image:
@ -139,7 +141,7 @@ class Fullweather(inkycal_module):
# Check if all required parameters are present # Check if all required parameters are present
for param in self.requires: for param in self.requires:
if not param in config: if param not in config:
raise Exception(f"config is missing {param}") raise Exception(f"config is missing {param}")
# required parameters # required parameters

View File

@ -8,13 +8,13 @@ from inkycal.custom import *
# PIL has a class named Image, use alias for Inkyimage -> Images # 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.inky_image import Inkyimage as Images, image_to_palette
from inkycal.modules.template import inkycal_module from inkycal.modules.template import inkycal_module
from inkycal.utils.json_cache import JSONCache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Slideshow(inkycal_module): 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" name = "Slideshow - cycle through images from a local folder"
requires = { requires = {
@ -53,7 +53,7 @@ class Slideshow(inkycal_module):
# required parameters # required parameters
for param in self.requires: for param in self.requires:
if not param in config: if param not in config:
raise Exception(f'config is missing {param}') raise Exception(f'config is missing {param}')
# optional parameters # optional parameters
@ -64,14 +64,15 @@ class Slideshow(inkycal_module):
# Get the full path of all png/jpg/jpeg images in the given folder # Get the full path of all png/jpg/jpeg images in the given folder
all_files = glob.glob(f'{self.path}/*') all_files = glob.glob(f'{self.path}/*')
self.images = [i for i in all_files self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
if not self.images: if not self.images:
logger.error('No images found in the given folder, please ' logger.error('No images found in the given folder, please double check your path!')
'double check your path!')
raise Exception('No images found in the given folder 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 # set a 'first run' signal
self._first_run = True self._first_run = True
@ -89,14 +90,16 @@ class Slideshow(inkycal_module):
logger.info(f'Image size: {im_size}') logger.info(f'Image size: {im_size}')
# rotates list items by 1 index # rotates list items by 1 index
def rotate(somelist): def rotate(list: list):
return somelist[1:] + somelist[:1] return list[1:] + list[:1]
# Switch to the next image if this is not the first run # Switch to the next image if this is not the first run
if self._first_run: if self._first_run:
self._first_run = False self._first_run = False
self.cache_data["current_index"] = 0
else: else:
self.images = rotate(self.images) self.images = rotate(self.images)
self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images)
# initialize custom image class # initialize custom image class
im = Images() im = Images()
@ -110,7 +113,7 @@ class Slideshow(inkycal_module):
# Remove background if present # Remove background if present
im.remove_alpha() im.remove_alpha()
# if autoflip was enabled, flip the image # if auto-flip was enabled, flip the image
if self.autoflip: if self.autoflip:
im.autoflip(self.orientation) im.autoflip(self.orientation)
@ -123,6 +126,8 @@ class Slideshow(inkycal_module):
# with the images now send, clear the current image # with the images now send, clear the current image
im.clear() im.clear()
self.cache.write(self.cache_data)
# return images # return images
return im_black, im_colour return im_black, im_colour

View File

@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
settings = Settings()
class Xkcd(inkycal_module): class Xkcd(inkycal_module):
name = "xkcd - Displays comics from xkcd.com by Randall Munroe" name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
@ -57,7 +59,7 @@ class Xkcd(inkycal_module):
"""Generate image for this module""" """Generate image for this module"""
# Create tmp path # Create tmp path
tmpPath = f"{top_level}/temp" tmpPath = settings.TEMPORARY_FOLDER
if not os.path.exists(tmpPath): if not os.path.exists(tmpPath):
os.mkdir(tmpPath) os.mkdir(tmpPath)

20
inkycal/settings.py Normal file
View 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"

View 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)