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