From 9346fcf750ad7612f4f045fe2b4c1f6e639afaef Mon Sep 17 00:00:00 2001
From: Ace
Date: Sat, 11 May 2024 23:25:40 +0200
Subject: [PATCH 01/62] code quality improvements
---
inkycal/__init__.py | 2 +-
inkycal/display/display.py | 6 +-----
inkycal/display/drivers/epdconfig.py | 3 ---
inkycal/modules/inkycal_calendar.py | 6 +++---
tests/test_inkycal_agenda.py | 2 +-
tests/test_inkycal_webshot.py | 10 ++++++++--
6 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/inkycal/__init__.py b/inkycal/__init__.py
index 0c6a244..6c721f5 100644
--- a/inkycal/__init__.py
+++ b/inkycal/__init__.py
@@ -14,7 +14,7 @@ import inkycal.modules.inkycal_stocks
import inkycal.modules.inkycal_webshot
import inkycal.modules.inkycal_xkcd
import inkycal.modules.inkycal_fullweather
+import inkycal.modules.inkycal_mawaqit
# Main file
from inkycal.main import Inkycal
-import inkycal.modules.inkycal_stocks
diff --git a/inkycal/display/display.py b/inkycal/display/display.py
index 89fdf4c..179f71f 100644
--- a/inkycal/display/display.py
+++ b/inkycal/display/display.py
@@ -2,13 +2,11 @@
Inkycal ePaper driving functions
Copyright by aceisace
"""
-import os
from importlib import import_module
import PIL
from PIL import Image
-from inkycal.custom import top_level
from inkycal.display.supported_models import supported_models
@@ -199,9 +197,7 @@ class Display:
>>> Display.get_display_names()
"""
- driver_files = top_level + '/inkycal/display/drivers/'
- drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i]
- return drivers
+ return supported_models.keys()
if __name__ == '__main__':
diff --git a/inkycal/display/drivers/epdconfig.py b/inkycal/display/drivers/epdconfig.py
index 2eaddf4..2a5e819 100644
--- a/inkycal/display/drivers/epdconfig.py
+++ b/inkycal/display/drivers/epdconfig.py
@@ -28,8 +28,6 @@ THE SOFTWARE.
"""
import logging
-import os
-import subprocess
import sys
import time
@@ -128,4 +126,3 @@ implementation = RaspberryPi()
for func in [x for x in dir(implementation) if not x.startswith('_')]:
setattr(sys.modules[__name__], func, getattr(implementation, func))
-
diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py
index 9c85984..8d0cd50 100755
--- a/inkycal/modules/inkycal_calendar.py
+++ b/inkycal/modules/inkycal_calendar.py
@@ -109,7 +109,7 @@ class Calendar(inkycal_module):
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
text_bbox_height = self.font.getbbox("hg")
- weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
+ weekdays_height = int((text_bbox_height[3] - text_bbox_height[1]) * 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
@@ -265,7 +265,7 @@ class Calendar(inkycal_module):
# find out how many lines can fit at max in the event section
line_spacing = 2
text_bbox_height = self.font.getbbox("hg")
- line_height = text_bbox_height[3] + line_spacing
+ line_height = text_bbox_height[3] - text_bbox_height[1] + line_spacing
max_event_lines = events_height // (line_height + line_spacing)
# generate list of coordinates for each line
@@ -326,7 +326,7 @@ class Calendar(inkycal_module):
(icon_width, icon_height),
radius=6,
thickness=1,
- shrinkage=(0.4, 0.2),
+ shrinkage=(0, 0),
)
# Filter upcoming events until 4 weeks in the future
diff --git a/tests/test_inkycal_agenda.py b/tests/test_inkycal_agenda.py
index af002ec..642f654 100755
--- a/tests/test_inkycal_agenda.py
+++ b/tests/test_inkycal_agenda.py
@@ -28,7 +28,7 @@ tests = [
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
- "language": "en"
+ "language": "de"
}
},
{
diff --git a/tests/test_inkycal_webshot.py b/tests/test_inkycal_webshot.py
index 7105a12..b86c965 100755
--- a/tests/test_inkycal_webshot.py
+++ b/tests/test_inkycal_webshot.py
@@ -6,10 +6,15 @@ import logging
import unittest
from inkycal.modules import Webshot
+from inkycal.modules.inky_image import Inkyimage
+from tests import Config
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
+preview = Inkyimage.preview
+merge = Inkyimage.merge
+
tests = [
{
"position": 1,
@@ -60,6 +65,7 @@ class TestWebshot(unittest.TestCase):
for test in tests:
logger.info(f'test {tests.index(test) + 1} generating image..')
module = Webshot(test)
- module.generate_image()
+ im_black, im_colour = module.generate_image()
+ if Config.USE_PREVIEW:
+ preview(merge(im_black, im_colour))
logger.info('OK')
-
From 610f246c0276e4145218133d7b02f169213a1709 Mon Sep 17 00:00:00 2001
From: Ace
Date: Sun, 12 May 2024 02:00:26 +0200
Subject: [PATCH 02/62] checkpoint
---
{icons => fonts}/ui-icons/home_temp.png | Bin
{icons => fonts}/ui-icons/humidity.bmp | Bin
.../outline_thermostat_white_48dp.bmp | Bin
{icons => fonts}/ui-icons/rain-chance.bmp | Bin
{icons => fonts}/ui-icons/uv.bmp | Bin
{icons => fonts}/ui-icons/wind.bmp | Bin
inkycal/__init__.py | 13 +-
inkycal/custom/functions.py | 26 +--
inkycal/display/drivers/10_in_3.py | 14 +-
inkycal/display/drivers/7_in_8.py | 14 +-
inkycal/display/drivers/9_in_7.py | 12 +-
inkycal/loggers.py | 35 +++
inkycal/main.py | 206 ++++++++----------
inkycal/modules/inkycal_fullweather.py | 8 +-
inkycal/modules/inkycal_slideshow.py | 25 ++-
inkycal/modules/inkycal_xkcd.py | 4 +-
inkycal/settings.py | 20 ++
inkycal/utils/json_cache.py | 28 +++
18 files changed, 225 insertions(+), 180 deletions(-)
rename {icons => fonts}/ui-icons/home_temp.png (100%)
rename {icons => fonts}/ui-icons/humidity.bmp (100%)
rename {icons => fonts}/ui-icons/outline_thermostat_white_48dp.bmp (100%)
rename {icons => fonts}/ui-icons/rain-chance.bmp (100%)
rename {icons => fonts}/ui-icons/uv.bmp (100%)
rename {icons => fonts}/ui-icons/wind.bmp (100%)
create mode 100644 inkycal/loggers.py
create mode 100644 inkycal/settings.py
create mode 100644 inkycal/utils/json_cache.py
diff --git a/icons/ui-icons/home_temp.png b/fonts/ui-icons/home_temp.png
similarity index 100%
rename from icons/ui-icons/home_temp.png
rename to fonts/ui-icons/home_temp.png
diff --git a/icons/ui-icons/humidity.bmp b/fonts/ui-icons/humidity.bmp
similarity index 100%
rename from icons/ui-icons/humidity.bmp
rename to fonts/ui-icons/humidity.bmp
diff --git a/icons/ui-icons/outline_thermostat_white_48dp.bmp b/fonts/ui-icons/outline_thermostat_white_48dp.bmp
similarity index 100%
rename from icons/ui-icons/outline_thermostat_white_48dp.bmp
rename to fonts/ui-icons/outline_thermostat_white_48dp.bmp
diff --git a/icons/ui-icons/rain-chance.bmp b/fonts/ui-icons/rain-chance.bmp
similarity index 100%
rename from icons/ui-icons/rain-chance.bmp
rename to fonts/ui-icons/rain-chance.bmp
diff --git a/icons/ui-icons/uv.bmp b/fonts/ui-icons/uv.bmp
similarity index 100%
rename from icons/ui-icons/uv.bmp
rename to fonts/ui-icons/uv.bmp
diff --git a/icons/ui-icons/wind.bmp b/fonts/ui-icons/wind.bmp
similarity index 100%
rename from icons/ui-icons/wind.bmp
rename to fonts/ui-icons/wind.bmp
diff --git a/inkycal/__init__.py b/inkycal/__init__.py
index 6c721f5..edb3926 100644
--- a/inkycal/__init__.py
+++ b/inkycal/__init__.py
@@ -1,20 +1,15 @@
-# Display class (for driving E-Paper displays)
-from inkycal.display import Display
-
# Default modules
import inkycal.modules.inkycal_agenda
import inkycal.modules.inkycal_calendar
-import inkycal.modules.inkycal_weather
import inkycal.modules.inkycal_feeds
-import inkycal.modules.inkycal_todoist
+import inkycal.modules.inkycal_fullweather
import inkycal.modules.inkycal_image
import inkycal.modules.inkycal_jokes
import inkycal.modules.inkycal_slideshow
import inkycal.modules.inkycal_stocks
+import inkycal.modules.inkycal_todoist
+import inkycal.modules.inkycal_weather
import inkycal.modules.inkycal_webshot
import inkycal.modules.inkycal_xkcd
-import inkycal.modules.inkycal_fullweather
-import inkycal.modules.inkycal_mawaqit
-
-# Main file
+from inkycal.display import Display
from inkycal.main import Inkycal
diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py
index 327095c..206a2ec 100644
--- a/inkycal/custom/functions.py
+++ b/inkycal/custom/functions.py
@@ -17,20 +17,16 @@ from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
-logs = logging.getLogger(__name__)
-logs.setLevel(level=logging.INFO)
+from inkycal.settings import Settings
-# Get the path to the Inkycal folder
-top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1])
+logger = logging.getLogger(__name__)
-# Get path of 'fonts' and 'images' folders within Inkycal folder
-fonts_location = os.path.join(top_level, "fonts/")
-image_folder = os.path.join(top_level, "image_folder/")
+settings = Settings()
# Get available fonts within fonts folder
fonts = {}
-for path, dirs, files in os.walk(fonts_location):
+for path, dirs, files in os.walk(settings.FONT_PATH):
for _ in files:
if _.endswith(".otf"):
name = _.split(".otf")[0]
@@ -39,7 +35,7 @@ for path, dirs, files in os.walk(fonts_location):
if _.endswith(".ttf"):
name = _.split(".ttf")[0]
fonts[name] = os.path.join(path, _)
-logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
+logger.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
available_fonts = [key for key, values in fonts.items()]
@@ -81,12 +77,12 @@ def get_system_tz() -> str:
"""
try:
local_tz = tzlocal.get_localzone().key
- logs.debug(f"Local system timezone is {local_tz}.")
+ logger.debug(f"Local system timezone is {local_tz}.")
except:
- logs.error("System timezone could not be parsed!")
- logs.error("Please set timezone manually!. Falling back to UTC...")
+ logger.error("System timezone could not be parsed!")
+ logger.error("Please set timezone manually!. Falling back to UTC...")
local_tz = "UTC"
- logs.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
+ logger.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
return local_tz
@@ -182,14 +178,14 @@ def write(image, xy, box_size, text, font=None, **kwargs):
# Truncate text if text is too long, so it can fit inside the box
if (text_width, text_height) > (box_width, box_height):
- logs.debug(("truncating {}".format(text)))
+ logger.debug(("truncating {}".format(text)))
while (text_width, text_height) > (box_width, box_height):
text = text[0:-1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1]
- logs.debug(text)
+ logger.debug(text)
# Align text to desired position
if alignment == "center" or None:
diff --git a/inkycal/display/drivers/10_in_3.py b/inkycal/display/drivers/10_in_3.py
index 834e2df..7d7b3fa 100644
--- a/inkycal/display/drivers/10_in_3.py
+++ b/inkycal/display/drivers/10_in_3.py
@@ -2,22 +2,18 @@
10.3" driver class
Copyright by aceinnolab
"""
+import os
from subprocess import run
from PIL import Image
-from inkycal.custom import image_folder, top_level
+from inkycal.settings import Settings
# Display resolution
EPD_WIDTH = 1872
EPD_HEIGHT = 1404
-# Please insert VCOM of your display. The Minus sign before is not required
-VCOM = "2.0"
-
-driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
-
-command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
+settings = Settings()
class EPD:
@@ -40,8 +36,8 @@ class EPD:
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
- image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
- command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
+ image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP')
+ command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
print(command)
return command
diff --git a/inkycal/display/drivers/7_in_8.py b/inkycal/display/drivers/7_in_8.py
index fe4d243..5f9cf51 100644
--- a/inkycal/display/drivers/7_in_8.py
+++ b/inkycal/display/drivers/7_in_8.py
@@ -2,20 +2,16 @@
7.8" parallel driver class
Copyright by aceinnolab
"""
+import os
from subprocess import run
-from inkycal.custom import image_folder, top_level
+from inkycal.settings import Settings
# Display resolution
EPD_WIDTH = 1872
EPD_HEIGHT = 1404
-# Please insert VCOM of your display. The Minus sign before is not required
-VCOM = "2.0"
-
-driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
-
-command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
+settings = Settings()
class EPD:
@@ -38,8 +34,8 @@ class EPD:
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True)
- image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
- command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
+ image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP')
+ command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
print(command)
return command
diff --git a/inkycal/display/drivers/9_in_7.py b/inkycal/display/drivers/9_in_7.py
index 8c6ec0f..c81feef 100644
--- a/inkycal/display/drivers/9_in_7.py
+++ b/inkycal/display/drivers/9_in_7.py
@@ -4,18 +4,16 @@ Copyright by aceinnolab
"""
from subprocess import run
-from inkycal.custom import image_folder, top_level
+from inkycal.settings import Settings
# Display resolution
EPD_WIDTH = 1200
EPD_HEIGHT = 825
-# Please insert VCOM of your display. The Minus sign before is not required
-VCOM = "2.0"
-driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
+settings = Settings()
-command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
+command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
class EPD:
@@ -38,8 +36,8 @@ class EPD:
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True)
- image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
- command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
+ image.convert('RGB').save(settings.IMAGE_FOLDER + 'canvas.bmp', 'BMP')
+ command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
print(command)
return command
diff --git a/inkycal/loggers.py b/inkycal/loggers.py
new file mode 100644
index 0000000..97c1c46
--- /dev/null
+++ b/inkycal/loggers.py
@@ -0,0 +1,35 @@
+"""Logging configuration for Inkycal."""
+import logging
+import os
+from logging.handlers import RotatingFileHandler
+
+from inkycal.settings import Settings
+
+# On the console, set a logger to show only important logs
+# (level ERROR or higher)
+stream_handler = logging.StreamHandler()
+stream_handler.setLevel(logging.ERROR)
+
+settings = Settings()
+
+if not os.path.exists(settings.LOG_PATH):
+ os.mkdir(settings.LOG_PATH)
+
+
+# Save all logs to a file, which contains more detailed output
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
+ datefmt='%d-%m-%Y %H:%M:%S',
+ handlers=[
+ stream_handler, # add stream handler from above
+ RotatingFileHandler( # log to a file too
+ settings.INKYCAL_LOG_PATH, # file to log
+ maxBytes=2*1024*1024, # 2MB max filesize
+ backupCount=5 # create max 5 log files
+ )
+ ]
+)
+
+# Show less logging for PIL module
+logging.getLogger("PIL").setLevel(logging.WARNING)
\ No newline at end of file
diff --git a/inkycal/main.py b/inkycal/main.py
index cc4473b..ac3dd88 100644
--- a/inkycal/main.py
+++ b/inkycal/main.py
@@ -6,44 +6,21 @@ Copyright by aceinnolab
import asyncio
import glob
import hashlib
-from logging.handlers import RotatingFileHandler
import numpy
+from inkycal import loggers # noqa
from inkycal.custom import *
from inkycal.display import Display
from inkycal.modules.inky_image import Inkyimage as Images
-
-# On the console, set a logger to show only important logs
-# (level ERROR or higher)
-stream_handler = logging.StreamHandler()
-stream_handler.setLevel(logging.ERROR)
-
-if not os.path.exists(f'{top_level}/logs'):
- os.mkdir(f'{top_level}/logs')
-
-# Save all logs to a file, which contains more detailed output
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
- datefmt='%d-%m-%Y %H:%M:%S',
- handlers=[
- stream_handler, # add stream handler from above
- RotatingFileHandler( # log to a file too
- f'{top_level}/logs/inkycal.log', # file to log
- maxBytes=2097152, # 2MB max filesize
- backupCount=5 # create max 5 log files
- )
- ]
-)
-
-# Show less logging for PIL module
-logging.getLogger("PIL").setLevel(logging.WARNING)
+from inkycal.utils.json_cache import JSONCache
logger = logging.getLogger(__name__)
+settings = Settings()
+
+CACHE_NAME = "inkycal_main"
-# TODO: autostart -> supervisor?
class Inkycal:
"""Inkycal main class
@@ -62,13 +39,7 @@ class Inkycal:
def __init__(self, settings_path: str or None = None, render: bool = True):
"""Initialise Inkycal"""
-
- # Get the release version from setup.py
- with open(f'{top_level}/setup.py') as setup_file:
- for line in setup_file:
- if line.startswith('__version__'):
- self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "")
- break
+ self._release = "2.0.3"
self.render = render
self.info = None
@@ -77,8 +48,7 @@ class Inkycal:
if settings_path:
try:
with open(settings_path) as settings_file:
- settings = json.load(settings_file)
- self.settings = settings
+ self.settings = json.load(settings_file)
except FileNotFoundError:
raise FileNotFoundError(
@@ -86,17 +56,19 @@ class Inkycal:
else:
try:
- with open('/boot/settings.json') as settings_file:
- settings = json.load(settings_file)
- self.settings = settings
+ with open('/boot/settings.json', mode="r") as settings_file:
+ self.settings = json.load(settings_file)
except FileNotFoundError:
raise SettingsFileNotFoundError
self.disable_calibration = self.settings.get('disable_calibration', False)
- if not os.path.exists(image_folder):
- os.mkdir(image_folder)
+ if not os.path.exists(settings.IMAGE_FOLDER):
+ os.mkdir(settings.IMAGE_FOLDER)
+
+ if not os.path.exists(settings.CACHE_PATH):
+ os.mkdir(settings.CACHE_PATH)
# Option to use epaper image optimisation, reduces colours
self.optimize = True
@@ -122,7 +94,7 @@ class Inkycal:
# Load and initialise modules specified in the settings file
self._module_number = 1
- for module in settings['modules']:
+ for module in self.settings['modules']:
module_name = module['name']
try:
loader = f'from inkycal.modules import {module_name}'
@@ -131,10 +103,9 @@ class Inkycal:
setup = f'self.module_{self._module_number} = {module_name}({module})'
# print(setup)
exec(setup)
- logger.info(('name : {name} size : {width}x{height} px'.format(
- name=module_name,
- width=module['config']['size'][0],
- height=module['config']['size'][1])))
+ width = module['config']['size'][0]
+ height = module['config']['size'][1]
+ logger.info(f'name : {module_name} size : {width}x{height} px')
self._module_number += 1
@@ -147,55 +118,56 @@ class Inkycal:
logger.exception(f"Exception: {traceback.format_exc()}.")
# Path to store images
- self.image_folder = image_folder
+ self.image_folder = settings.IMAGE_FOLDER
# Remove old hashes
self._remove_hashes(self.image_folder)
+ # set up cache
+ if not os.path.exists(os.path.join(settings.CACHE_PATH, CACHE_NAME)):
+ if not os.path.exists(settings.CACHE_PATH):
+ os.mkdir(settings.CACHE_PATH)
+ self.cache = JSONCache(CACHE_NAME)
+ self.cache_data = self.cache.read()
+
# Give an OK message
print('loaded inkycal')
- def countdown(self, interval_mins: int or None = None) -> int:
- """Returns the remaining time in seconds until next display update.
+ def countdown(self, interval_mins: int = None) -> int:
+ """Returns the remaining time in seconds until the next display update based on the interval.
Args:
- - interval_mins = int -> the interval in minutes for the update
- if no interval is given, the value from the settings file is used.
+ interval_mins (int): The interval in minutes for the update. If none is given, the value
+ from the settings file is used.
Returns:
- - int -> the remaining time in seconds until next update
+ int: The remaining time in seconds until the next update.
"""
-
- # Check if empty, if empty, use value from settings file
+ # Default to settings if no interval is provided
if interval_mins is None:
interval_mins = self.settings["update_interval"]
- # Find out at which minutes the update should happen
+ # Get the current time
now = arrow.now()
- if interval_mins <= 60:
- update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1]
- # Calculate time in minutes until next update
- minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
+ # Calculate the next update time
+ # Finding the total minutes from the start of the day
+ minutes_since_midnight = now.hour * 60 + now.minute
- # Print the remaining time in minutes until next update
- print(f'{minutes} minutes left until next refresh')
+ # Finding the next interval point
+ minutes_to_next_interval = (
+ minutes_since_midnight // interval_mins + 1) * interval_mins - minutes_since_midnight
+ seconds_to_next_interval = minutes_to_next_interval * 60 - now.second
- # Calculate time in seconds until next update
- remaining_time = minutes * 60 + (60 - now.second)
-
- # Return seconds until next update
- return remaining_time
+ # Logging the remaining time in appropriate units
+ hours_to_next_interval = minutes_to_next_interval // 60
+ remaining_minutes = minutes_to_next_interval % 60
+ if hours_to_next_interval > 0:
+ print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh')
else:
- # Calculate time in minutes until next update using the range of 24 hours in steps of every full hour
- update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1]
- minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
- remaining_time = minutes * 60 + (60 - now.second)
+ print(f'{remaining_minutes} minutes left until next refresh')
- print(f'{round(minutes / 60, 1)} hours left until next refresh')
-
- # Return seconds until next update
- return remaining_time
+ return seconds_to_next_interval
def test(self):
"""Tests if Inkycal can run without issues.
@@ -218,20 +190,13 @@ class Inkycal:
for number in range(1, self._module_number):
name = eval(f"self.module_{number}.name")
- module = eval(f'self.module_{number}')
print(f'generating image(s) for {name}...', end="")
- try:
- black, colour = module.generate_image()
- if self.show_border:
- draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
- black.save(f"{self.image_folder}module{number}_black.png", "PNG")
- colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
+ success = self.process_module(number)
+ if success:
print("OK!")
- except Exception:
+ else:
errors.append(number)
self.info += f"module {number}: Error! "
- logger.exception("Error!")
- logger.exception(f"Exception: {traceback.format_exc()}.")
if errors:
logger.error('Error/s in modules:', *errors)
@@ -277,14 +242,17 @@ class Inkycal:
print("Refresh needed: {a}".format(a=res))
return res
- async def run(self):
- """Runs main program in nonstop mode.
+ async def run(self, run_once=False):
+ """Runs main program in nonstop mode or a single iteration based on the run_once flag.
- Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
- from all modules, assembles them in one image, refreshed the E-Paper and
- then sleeps until the next scheduled update.
+ Args:
+ run_once (bool): If True, runs the updating process once and stops. If False,
+ runs indefinitely.
+
+ Uses an infinity loop to run Inkycal nonstop or a single time based on run_once.
+ Inkycal generates the image from all modules, assembles them in one image,
+ refreshes the E-Paper and then sleeps until the next scheduled update or exits.
"""
-
# Get the time of initial run
runtime = arrow.now()
@@ -303,31 +271,19 @@ class Inkycal:
f"Time: {current_time.format('HH:mm')}")
print('Generating images for all modules...', end='')
- errors = [] # store module numbers in here
+ errors = [] # Store module numbers in here
- # short info for info-section
+ # Short info for info-section
if not self.settings.get('image_hash', False):
self.info = f"{current_time.format('D MMM @ HH:mm')} "
else:
self.info = ""
for number in range(1, self._module_number):
-
- # name = eval(f"self.module_{number}.name")
- module = eval(f'self.module_{number}')
-
- try:
- black, colour = module.generate_image()
- if self.show_border:
- draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
- black.save(f"{self.image_folder}module{number}_black.png", "PNG")
- colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
- self.info += f"module {number}: OK "
- except Exception as e:
+ success = self.process_module(number)
+ if not success:
errors.append(number)
self.info += f"module {number}: Error! "
- logger.exception("Error!")
- logger.exception(f"Exception: {traceback.format_exc()}.")
if errors:
logger.error("Error/s in modules:", *errors)
@@ -343,10 +299,9 @@ class Inkycal:
# Check if image should be rendered
if self.render:
display = self.Display
-
self._calibration_check()
if self._calibration_state:
- # after calibration, we have to forcefully rewrite the screen
+ # After calibration, we have to forcefully rewrite the screen
self._remove_hashes(self.image_folder)
if self.supports_colour:
@@ -358,17 +313,15 @@ class Inkycal:
im_black = upside_down(im_black)
im_colour = upside_down(im_colour)
- # render the image on the display
+ # Render the image on the display
if not self.settings.get('image_hash', False) or self._needs_image_update([
(f"{self.image_folder}/canvas.png.hash", im_black),
(f"{self.image_folder}/canvas_colour.png.hash", im_colour)
]):
- # render the image on the display
display.render(im_black, im_colour)
# Part for black-white ePapers
elif not self.supports_colour:
-
im_black = self._merge_bands()
# Flip the image by 180° if required
@@ -376,13 +329,15 @@ class Inkycal:
im_black = upside_down(im_black)
if not self.settings.get('image_hash', False) or self._needs_image_update([
- (f"{self.image_folder}/canvas.png.hash", im_black),
- ]):
+ (f"{self.image_folder}/canvas.png.hash", im_black),]):
display.render(im_black)
print(f'\nNo errors since {counter} display updates \n'
f'program started {runtime.humanize()}')
+ if run_once:
+ break # Exit the loop after one full cycle if run_once is True
+
sleep_time = self.countdown()
await asyncio.sleep(sleep_time)
@@ -392,7 +347,8 @@ class Inkycal:
returns the merged image
"""
- im1_path, im2_path = image_folder + 'canvas.png', image_folder + 'canvas_colour.png'
+ im1_path = os.path.join(settings.image_folder, "canvas.png")
+ im2_path = os.path.join(settings.image_folder, "canvas_colour.png")
# If there is an image for black and colour, merge them
if os.path.exists(im1_path) and os.path.exists(im2_path):
@@ -531,7 +487,7 @@ class Inkycal:
im_colour = black_to_colour(im_colour)
im_colour.paste(im_black, (0, 0), im_black)
- im_colour.save(image_folder + 'full-screen.png', 'PNG')
+ im_colour.save(os.path.join(settings.IMAGE_FOLDER, 'full-screen.png'), 'PNG')
@staticmethod
def _optimize_im(image, threshold=220):
@@ -574,13 +530,29 @@ class Inkycal:
@staticmethod
def cleanup():
# clean up old images in image_folder
- for _file in glob.glob(f"{image_folder}*.png"):
+ if len(glob.glob(settings.IMAGE_FOLDER)) <= 1:
+ return
+ for _file in glob.glob(settings.IMAGE_FOLDER):
try:
os.remove(_file)
except:
logger.error(f"could not remove file: {_file}")
pass
+ def process_module(self, number) -> bool or Exception:
+ """Process individual module to generate images and handle exceptions."""
+ module = eval(f'self.module_{number}')
+ try:
+ black, colour = module.generate_image()
+ if self.show_border:
+ draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
+ black.save(f"{self.image_folder}module{number}_black.png", "PNG")
+ colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
+ return True
+ except Exception:
+ logger.exception(f"Error in module {number}!")
+ return False
+
if __name__ == '__main__':
print(f'running inkycal main in standalone/debug mode')
diff --git a/inkycal/modules/inkycal_fullweather.py b/inkycal/modules/inkycal_fullweather.py
index 55ce044..e48f17f 100644
--- a/inkycal/modules/inkycal_fullweather.py
+++ b/inkycal/modules/inkycal_fullweather.py
@@ -23,16 +23,18 @@ from icons.weather_icons.weather_icons import get_weather_icon
from inkycal.custom.functions import fonts
from inkycal.custom.functions import get_system_tz
from inkycal.custom.functions import internet_available
-from inkycal.custom.functions import top_level
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
from inkycal.modules.inky_image import image_to_palette
from inkycal.modules.template import inkycal_module
+from inkycal.settings import Settings
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
-icons_dir = os.path.join(top_level, "icons", "ui-icons")
+settings = Settings()
+
+icons_dir = os.path.join(settings.FONT_PATH, "ui-icons")
def outline(image: Image, size: int, color: tuple) -> Image:
@@ -139,7 +141,7 @@ class Fullweather(inkycal_module):
# Check if all required parameters are present
for param in self.requires:
- if not param in config:
+ if param not in config:
raise Exception(f"config is missing {param}")
# required parameters
diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py
index c7481fa..9f098df 100755
--- a/inkycal/modules/inkycal_slideshow.py
+++ b/inkycal/modules/inkycal_slideshow.py
@@ -8,13 +8,13 @@ from inkycal.custom import *
# PIL has a class named Image, use alias for Inkyimage -> Images
from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette
from inkycal.modules.template import inkycal_module
+from inkycal.utils.json_cache import JSONCache
logger = logging.getLogger(__name__)
class Slideshow(inkycal_module):
- """Cycles through images in a local image folder
- """
+ """Cycles through images in a local image folder"""
name = "Slideshow - cycle through images from a local folder"
requires = {
@@ -53,7 +53,7 @@ class Slideshow(inkycal_module):
# required parameters
for param in self.requires:
- if not param in config:
+ if param not in config:
raise Exception(f'config is missing {param}')
# optional parameters
@@ -64,14 +64,15 @@ class Slideshow(inkycal_module):
# Get the full path of all png/jpg/jpeg images in the given folder
all_files = glob.glob(f'{self.path}/*')
- self.images = [i for i in all_files
- if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
+ self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
if not self.images:
- logger.error('No images found in the given folder, please '
- 'double check your path!')
+ logger.error('No images found in the given folder, please double check your path!')
raise Exception('No images found in the given folder path :/')
+ self.cache = JSONCache('inkycal_slideshow')
+ self.cache_data = self.cache.read()
+
# set a 'first run' signal
self._first_run = True
@@ -89,14 +90,16 @@ class Slideshow(inkycal_module):
logger.info(f'Image size: {im_size}')
# rotates list items by 1 index
- def rotate(somelist):
- return somelist[1:] + somelist[:1]
+ def rotate(list: list):
+ return list[1:] + list[:1]
# Switch to the next image if this is not the first run
if self._first_run:
self._first_run = False
+ self.cache_data["current_index"] = 0
else:
self.images = rotate(self.images)
+ self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images)
# initialize custom image class
im = Images()
@@ -110,7 +113,7 @@ class Slideshow(inkycal_module):
# Remove background if present
im.remove_alpha()
- # if autoflip was enabled, flip the image
+ # if auto-flip was enabled, flip the image
if self.autoflip:
im.autoflip(self.orientation)
@@ -123,6 +126,8 @@ class Slideshow(inkycal_module):
# with the images now send, clear the current image
im.clear()
+ self.cache.write(self.cache_data)
+
# return images
return im_black, im_colour
diff --git a/inkycal/modules/inkycal_xkcd.py b/inkycal/modules/inkycal_xkcd.py
index b2ce25e..63f7140 100644
--- a/inkycal/modules/inkycal_xkcd.py
+++ b/inkycal/modules/inkycal_xkcd.py
@@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
+settings = Settings()
+
class Xkcd(inkycal_module):
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
@@ -57,7 +59,7 @@ class Xkcd(inkycal_module):
"""Generate image for this module"""
# Create tmp path
- tmpPath = f"{top_level}/temp"
+ tmpPath = settings.TEMPORARY_FOLDER
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
diff --git a/inkycal/settings.py b/inkycal/settings.py
new file mode 100644
index 0000000..8f40352
--- /dev/null
+++ b/inkycal/settings.py
@@ -0,0 +1,20 @@
+"""Settings class
+Used to initialize the settings for the application.
+"""
+import os
+
+basedir = os.path.abspath(os.path.dirname(__file__))
+
+
+class Settings:
+ """Settings class to initialize the settings for the application.
+
+ """
+ CACHE_PATH = os.path.join(basedir, "cache")
+ LOG_PATH = os.path.join(basedir, "logs")
+ INKYCAL_LOG_PATH = os.path.join(LOG_PATH, "inkycal.log")
+ FONT_PATH = os.path.join(basedir, "../fonts")
+ IMAGE_FOLDER = os.path.join(basedir, "../image_folder")
+ PARALLEL_DRIVER_PATH = os.path.join(basedir, "inkycal", "display", "drivers", "parallel_drivers")
+ TEMPORARY_FOLDER = os.path.join(basedir, "tmp")
+ VCOM = "2.0"
diff --git a/inkycal/utils/json_cache.py b/inkycal/utils/json_cache.py
new file mode 100644
index 0000000..76e8e19
--- /dev/null
+++ b/inkycal/utils/json_cache.py
@@ -0,0 +1,28 @@
+"""JSON Cache
+Can be used to cache JSON data to disk. This is useful for caching data to survive reboots.
+"""
+import json
+import os
+
+from inkycal.settings import Settings
+
+settings = Settings()
+
+
+class JSONCache:
+ def __init__(self, name: str, create_if_not_exists: bool = True):
+ self.path = os.path.join(settings.CACHE_PATH,f"{name}.json")
+ if create_if_not_exists and not os.path.exists(self.path):
+ with open(self.path, "w", encoding="utf-8") as file:
+ json.dump({}, file)
+
+ def read(self):
+ try:
+ with open(self.path, "r", encoding="utf-8") as file:
+ return json.load(file)
+ except FileNotFoundError:
+ return {}
+
+ def write(self, data: dict):
+ with open(self.path, "w", encoding="utf-8") as file:
+ json.dump(data, file, indent=4, sort_keys=True)
From e7036093995906586289eb92dc8ac8f1d53f0821 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 17 Jun 2024 22:48:05 +0000
Subject: [PATCH 03/62] Bump urllib3 from 2.2.0 to 2.2.2
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.0 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.0...2.2.2)
---
updated-dependencies:
- dependency-name: urllib3
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 37d5b99..24d970a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -46,7 +46,7 @@ types-python-dateutil==2.8.19.20240106
typing_extensions==4.9.0
tzdata==2024.1
tzlocal==5.2
-urllib3==2.2.0
+urllib3==2.2.2
virtualenv==20.25.0
webencodings==0.5.1
x-wr-timezone==0.0.6
From bf9f7db5acdadfcb25ac269bc04686828d0c98ac Mon Sep 17 00:00:00 2001
From: Ace
Date: Thu, 20 Jun 2024 22:31:17 +0200
Subject: [PATCH 04/62] implement rotation for webshot module
---
inkycal/modules/inkycal_webshot.py | 31 +++++++++++++++----
tests/test_inkycal_webshot.py | 49 ++++++++++++++++--------------
2 files changed, 51 insertions(+), 29 deletions(-)
diff --git a/inkycal/modules/inkycal_webshot.py b/inkycal/modules/inkycal_webshot.py
index 8d08cc6..50b7fb0 100644
--- a/inkycal/modules/inkycal_webshot.py
+++ b/inkycal/modules/inkycal_webshot.py
@@ -41,7 +41,10 @@ class Webshot(inkycal_module):
},
"crop_h": {
"label": "Please enter the crop height",
- }
+ },
+ "rotation": {
+ "label": "Please enter the rotation. Must be either 0, 90, 180 or 270",
+ },
}
def __init__(self, config):
@@ -73,6 +76,12 @@ class Webshot(inkycal_module):
else:
self.crop_y = 0
+ self.rotation = 0
+ if "rotation" in config:
+ self.rotation = int(config["rotation"])
+ if self.rotation not in [0, 90, 180, 270]:
+ raise Exception("Rotation must be either 0, 90, 180 or 270")
+
# give an OK message
print(f'Inkycal webshot loaded')
@@ -105,7 +114,7 @@ class Webshot(inkycal_module):
logger.info(
f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}')
- shot = WebShot()
+ shot = WebShot(size=(im_height, im_width))
shot.params = {
"--crop-x": self.crop_x,
@@ -151,11 +160,21 @@ class Webshot(inkycal_module):
centerPosX = int((im_width / 2) - (im.image.width / 2))
- webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
- im_black.paste(webshotSpaceBlack)
- webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
- im_colour.paste(webshotSpaceColour)
+ if self.rotation != 0:
+ webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
+ im_black.paste(webshotSpaceBlack)
+ im_black = im_black.rotate(self.rotation, expand=True)
+
+ webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
+ im_colour.paste(webshotSpaceColour)
+ im_colour = im_colour.rotate(self.rotation, expand=True)
+ else:
+ webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
+ im_black.paste(webshotSpaceBlack)
+
+ webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
+ im_colour.paste(webshotSpaceColour)
im.clear()
logger.info(f'added webshot image')
diff --git a/tests/test_inkycal_webshot.py b/tests/test_inkycal_webshot.py
index 7105a12..d8e1b12 100755
--- a/tests/test_inkycal_webshot.py
+++ b/tests/test_inkycal_webshot.py
@@ -11,33 +11,13 @@ logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
tests = [
- {
- "position": 1,
- "name": "Webshot",
- "config": {
- "size": [400, 100],
- "url": "https://github.com",
- "palette": "bwr",
- "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
- }
- },
{
"position": 1,
"name": "Webshot",
"config": {
"size": [400, 200],
- "url": "https://github.com",
- "palette": "bwy",
- "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
- }
- },
- {
- "position": 1,
- "name": "Webshot",
- "config": {
- "size": [400, 300],
- "url": "https://github.com",
- "palette": "bw",
+ "url": "https://aceinnolab.com",
+ "palette": "bwr",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
@@ -46,8 +26,31 @@ tests = [
"name": "Webshot",
"config": {
"size": [400, 400],
- "url": "https://github.com",
+ "url": "https://aceinnolab.com",
+ "palette": "bwy",
+ "rotation": 0,
+ "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
+ }
+ },
+ {
+ "position": 1,
+ "name": "Webshot",
+ "config": {
+ "size": [400, 600],
+ "url": "https://aceinnolab.com",
+ "palette": "bw",
+ "rotation": 90,
+ "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
+ }
+ },
+ {
+ "position": 1,
+ "name": "Webshot",
+ "config": {
+ "size": [400, 800],
+ "url": "https://aceinnolab.com",
"palette": "bwr",
+ "rotation": 180,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
}
From acc4fb7ce64a56ba637b87f7ed93d663cc77f176 Mon Sep 17 00:00:00 2001
From: Ace
Date: Sat, 22 Jun 2024 02:03:48 +0200
Subject: [PATCH 05/62] fix an issue where the text would not be vertically
centered
---
inkycal/custom/functions.py | 51 ++++++++++++++++-------------
inkycal/modules/inkycal_calendar.py | 49 +++++++++++++--------------
tests/test_functions.py | 22 +++++++++----
tests/test_inkycal_calendar.py | 2 +-
4 files changed, 68 insertions(+), 56 deletions(-)
diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py
index 327095c..0bdb419 100644
--- a/inkycal/custom/functions.py
+++ b/inkycal/custom/functions.py
@@ -8,17 +8,17 @@ import logging
import os
import time
import traceback
+from typing import Tuple
import arrow
-import PIL
import requests
import tzlocal
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
-logs = logging.getLogger(__name__)
-logs.setLevel(level=logging.INFO)
+logger = logging.getLogger(__name__)
+logger.setLevel(level=logging.INFO)
# Get the path to the Inkycal folder
top_level = "/".join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/")[:-1])
@@ -39,7 +39,7 @@ for path, dirs, files in os.walk(fonts_location):
if _.endswith(".ttf"):
name = _.split(".ttf")[0]
fonts[name] = os.path.join(path, _)
-logs.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
+logger.debug(f"Found fonts: {json.dumps(fonts, indent=4, sort_keys=True)}")
available_fonts = [key for key, values in fonts.items()]
@@ -77,16 +77,16 @@ def get_system_tz() -> str:
>>> import arrow
>>> print(arrow.now()) # returns non-timezone-aware time
- >>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
+ >>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time.
"""
try:
local_tz = tzlocal.get_localzone().key
- logs.debug(f"Local system timezone is {local_tz}.")
+ logger.debug(f"Local system timezone is {local_tz}.")
except:
- logs.error("System timezone could not be parsed!")
- logs.error("Please set timezone manually!. Falling back to UTC...")
+ logger.error("System timezone could not be parsed!")
+ logger.error("Please set timezone manually!. Falling back to UTC...")
local_tz = "UTC"
- logs.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
+ logger.debug(f"The time is {arrow.now(tz=local_tz).format('YYYY-MM-DD HH:mm:ss ZZ')}.")
return local_tz
@@ -115,7 +115,7 @@ def auto_fontsize(font, max_height):
return font
-def write(image, xy, box_size, text, font=None, **kwargs):
+def write(image: Image, xy: Tuple[int, int], box_size: Tuple[int, int], text: str, font=None, **kwargs):
"""Writes text on an image.
Writes given text at given position on the specified image.
@@ -165,7 +165,7 @@ def write(image, xy, box_size, text, font=None, **kwargs):
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
- text_height = text_bbox_height[3] - text_bbox_height[1]
+ text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
while text_width < int(box_width * fill_width) and text_height < int(box_height * fill_height):
size += 1
@@ -173,23 +173,23 @@ def write(image, xy, box_size, text, font=None, **kwargs):
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
- text_height = text_bbox_height[3] - text_bbox_height[1]
+ text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
- text_height = text_bbox_height[3] - text_bbox_height[1]
+ text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
# Truncate text if text is too long, so it can fit inside the box
if (text_width, text_height) > (box_width, box_height):
- logs.debug(("truncating {}".format(text)))
+ logger.debug(("truncating {}".format(text)))
while (text_width, text_height) > (box_width, box_height):
text = text[0:-1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
- text_height = text_bbox_height[3] - text_bbox_height[1]
- logs.debug(text)
+ text_height = abs(text_bbox_height[3]) # - abs(text_bbox_height[1])
+ logger.debug(text)
# Align text to desired position
if alignment == "center" or None:
@@ -199,10 +199,13 @@ def write(image, xy, box_size, text, font=None, **kwargs):
elif alignment == "right":
x = int(box_width - text_width)
+ # Vertical centering
+ y = int((box_height / 2) - (text_height / 2))
+
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
- ImageDraw.Draw(space).text((x, 0), text, fill=colour, font=font)
+ ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
# Uncomment following two lines, comment out above two lines to show
# red text-box with white text (debugging purposes)
@@ -217,7 +220,7 @@ def write(image, xy, box_size, text, font=None, **kwargs):
image.paste(space, xy, space)
-def text_wrap(text, font=None, max_width=None):
+def text_wrap(text: str, font=None, max_width=None):
"""Splits a very long text into smaller parts
Splits a long text to smaller lines which can fit in a line with max_width.
@@ -253,7 +256,7 @@ def text_wrap(text, font=None, max_width=None):
return lines
-def internet_available():
+def internet_available() -> bool:
"""checks if the internet is available.
Attempts to connect to google.com with a timeout of 5 seconds to check
@@ -278,15 +281,16 @@ def internet_available():
return False
-def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
+def draw_border(image: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int = 5, thickness: int = 1,
+ shrinkage: Tuple[int, int] = (0.1, 0.1)) -> None:
"""Draws a border at given coordinates.
Args:
- image: The image on which the border should be drawn (usually im_black or
- im_colour.
+ im_colour).
- xy: Tuple representing the top-left corner of the border e.g. (32, 100)
- where 32 is the x co-ordinate and 100 is the y-coordinate.
+ where 32 is the x-coordinate and 100 is the y-coordinate.
- size: Size of the border as a tuple -> (width, height).
@@ -324,6 +328,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height)
c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height)
+
# Draw lines and arcs, creating a square with round corners
draw = ImageDraw.Draw(image)
draw.line((p1, p2), fill=colour, width=thickness)
@@ -338,7 +343,7 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)
-def draw_border_2(im: PIL.Image, xy: tuple, size: tuple, radius: int):
+def draw_border_2(im: Image, xy: Tuple[int, int], size: Tuple[int, int], radius: int):
draw = ImageDraw.Draw(im)
x, y = xy
diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py
index 9c85984..c17582f 100755
--- a/inkycal/modules/inkycal_calendar.py
+++ b/inkycal/modules/inkycal_calendar.py
@@ -6,16 +6,16 @@ Copyright by aceinnolab
# pylint: disable=logging-fstring-interpolation
import calendar as cal
-import arrow
-from inkycal.modules.template import inkycal_module
+
from inkycal.custom import *
+from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class Calendar(inkycal_module):
"""Calendar class
- Create monthly calendar and show events from given icalendars
+ Create monthly calendar and show events from given iCalendars
"""
name = "Calendar - Show monthly calendar with events from iCalendars"
@@ -39,12 +39,12 @@ class Calendar(inkycal_module):
},
"date_format": {
"label": "Use an arrow-supported token for custom date formatting "
- + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
+ + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
"default": "D MMM",
},
"time_format": {
"label": "Use an arrow-supported token for custom time formatting "
- + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
+ + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
"default": "HH:mm",
},
}
@@ -61,7 +61,7 @@ class Calendar(inkycal_module):
self._days_with_events = None
# optional parameters
- self.weekstart = config['week_starts_on']
+ self.week_start = config['week_starts_on']
self.show_events = config['show_events']
self.date_format = config["date_format"]
self.time_format = config['time_format']
@@ -109,7 +109,7 @@ class Calendar(inkycal_module):
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
text_bbox_height = self.font.getbbox("hg")
- weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
+ weekdays_height = int((abs(text_bbox_height[3]) + abs(text_bbox_height[1])) * 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
@@ -117,7 +117,7 @@ class Calendar(inkycal_module):
logger.debug("Allocating space for events")
calendar_height = int(im_height * 0.6)
events_height = (
- im_height - month_name_height - weekdays_height - calendar_height
+ im_height - month_name_height - weekdays_height - calendar_height
)
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
logger.debug(f'events-section size: {im_width} x {events_height} px')
@@ -156,13 +156,13 @@ class Calendar(inkycal_module):
now = arrow.now(tz=self.timezone)
- # Set weekstart of calendar to specified weekstart
- if self.weekstart == "Monday":
+ # Set week-start of calendar to specified week-start
+ if self.week_start == "Monday":
cal.setfirstweekday(cal.MONDAY)
- weekstart = now.shift(days=-now.weekday())
+ week_start = now.shift(days=-now.weekday())
else:
cal.setfirstweekday(cal.SUNDAY)
- weekstart = now.shift(days=-now.isoweekday())
+ week_start = now.shift(days=-now.isoweekday())
# Write the name of current month
write(
@@ -174,9 +174,9 @@ class Calendar(inkycal_module):
autofit=True,
)
- # Set up weeknames in local language and add to main section
+ # Set up week-names in local language and add to main section
weekday_names = [
- weekstart.shift(days=+_).format('ddd', locale=self.language)
+ week_start.shift(days=+_).format('ddd', locale=self.language)
for _ in range(7)
]
logger.debug(f'weekday names: {weekday_names}')
@@ -192,7 +192,7 @@ class Calendar(inkycal_module):
fill_height=0.9,
)
- # Create a calendar template and flatten (remove nestings)
+ # Create a calendar template and flatten (remove nesting)
calendar_flat = self.flatten(cal.monthcalendar(now.year, now.month))
# logger.debug(f" calendar_flat: {calendar_flat}")
@@ -281,7 +281,7 @@ class Calendar(inkycal_module):
month_start = arrow.get(now.floor('month'))
month_end = arrow.get(now.ceil('month'))
- # fetch events from given icalendars
+ # fetch events from given iCalendars
self.ical = iCalendar()
parser = self.ical
@@ -294,14 +294,12 @@ class Calendar(inkycal_module):
month_events = parser.get_events(month_start, month_end, self.timezone)
parser.sort()
self.month_events = month_events
-
+
# Initialize days_with_events as an empty list
days_with_events = []
# Handle multi-day events by adding all days between start and end
for event in month_events:
- start_date = event['begin'].date()
- end_date = event['end'].date()
# Convert start and end dates to arrow objects with timezone
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
@@ -325,8 +323,6 @@ class Calendar(inkycal_module):
grid[days],
(icon_width, icon_height),
radius=6,
- thickness=1,
- shrinkage=(0.4, 0.2),
)
# Filter upcoming events until 4 weeks in the future
@@ -345,13 +341,13 @@ class Calendar(inkycal_module):
date_width = int(max((
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
- for events in upcoming_events))* 1.1
- )
+ for events in upcoming_events)) * 1.1
+ )
time_width = int(max((
self.font.getlength(events['begin'].format(self.time_format, locale=lang))
- for events in upcoming_events))* 1.1
- )
+ for events in upcoming_events)) * 1.1
+ )
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
@@ -369,7 +365,8 @@ class Calendar(inkycal_module):
event_duration = (event['end'] - event['begin']).days
if event_duration > 1:
# Format the duration using Arrow's localization
- days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang)
+ days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True,
+ locale=lang)
the_name = f"{event['title']} ({days_translation})"
else:
the_name = event['title']
diff --git a/tests/test_functions.py b/tests/test_functions.py
index b624978..0d8a6dd 100644
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -1,12 +1,22 @@
"""
Test the functions in the functions module.
"""
+import unittest
+
from PIL import Image, ImageFont
-from inkycal.custom import write, fonts
+
+from inkycal.custom import write, fonts, get_system_tz
-def test_write():
- im = Image.new("RGB", (500, 200), "white")
- font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size = 40)
- write(im, (125,75), (250, 50), "Hello World", font)
- # im.show()
+class TestIcalendar(unittest.TestCase):
+
+ def test_write(self):
+ im = Image.new("RGB", (500, 200), "white")
+ font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], size=40)
+ write(im, (125, 75), (250, 50), "Hello World", font)
+ # im.show()
+
+ def test_get_system_tz(self):
+ tz = get_system_tz()
+ assert isinstance(tz, str)
+
diff --git a/tests/test_inkycal_calendar.py b/tests/test_inkycal_calendar.py
index cb28b9a..434d5d8 100755
--- a/tests/test_inkycal_calendar.py
+++ b/tests/test_inkycal_calendar.py
@@ -20,7 +20,7 @@ tests = [
{
"name": "Calendar",
"config": {
- "size": [500, 500],
+ "size": [500, 600],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": sample_url,
From 544f6063798f15b5ceceeb2bc8b4f8068a271fbb Mon Sep 17 00:00:00 2001
From: github-actions
Date: Sat, 22 Jun 2024 00:47:19 +0000
Subject: [PATCH 06/62] update docs [bot]
---
docs/inkycal.html | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/docs/inkycal.html b/docs/inkycal.html
index 2166d2b..e098b4e 100644
--- a/docs/inkycal.html
+++ b/docs/inkycal.html
@@ -232,14 +232,14 @@ which the given font should be scaled to.
-
-inkycal.custom.functions.draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1))
+inkycal.custom.functions.draw_border(image: <module 'PIL.Image' from '/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>, xy: ~typing.Tuple[int, int], size: ~typing.Tuple[int, int], radius: int = 5, thickness: int = 1, shrinkage: ~typing.Tuple[int, int] = (0.1, 0.1)) → None
Draws a border at given coordinates.
- Args:
image: The image on which the border should be drawn (usually im_black or
-im_colour.
+im_colour).
xy: Tuple representing the top-left corner of the border e.g. (32, 100)
-where 32 is the x co-ordinate and 100 is the y-coordinate.
+where 32 is the x-coordinate and 100 is the y-coordinate.
size: Size of the border as a tuple -> (width, height).
radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.
thickness: Thickness of the border in pixels.
@@ -288,14 +288,14 @@ printed fonts of this function:
The extracted timezone can be used to show the local time instead of UTC. e.g.
>>> import arrow
>>> print(arrow.now()) # returns non-timezone-aware time
->>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
+>>> print(arrow.now(tz=get_system_tz())) # prints timezone aware time.
-
-inkycal.custom.functions.internet_available()
+inkycal.custom.functions.internet_available() → bool
checks if the internet is available.
Attempts to connect to google.com with a timeout of 5 seconds to check
if the network can be reached.
@@ -315,7 +315,7 @@ if the network can be reached.
-
-inkycal.custom.functions.text_wrap(text, font=None, max_width=None)
+inkycal.custom.functions.text_wrap(text: str, font=None, max_width=None)
Splits a very long text into smaller parts
Splits a long text to smaller lines which can fit in a line with max_width.
Uses a Font object for more accurate calculations.
@@ -334,7 +334,7 @@ splitting the text into the next chunk.
-
-inkycal.custom.functions.write(image, xy, box_size, text, font=None, **kwargs)
+inkycal.custom.functions.write(image: <module 'PIL.Image' from '/home/runner/work/Inkycal/Inkycal/venv/lib/python3.11/site-packages/PIL/Image.py'>, xy: ~typing.Tuple[int, int], box_size: ~typing.Tuple[int, int], text: str, font=None, **kwargs)
Writes text on an image.
Writes given text at given position on the specified image.
From 4f7d31f54e822e915323fbef2d29882bd98a82e5 Mon Sep 17 00:00:00 2001
From: Ace
Date: Sat, 22 Jun 2024 02:56:45 +0200
Subject: [PATCH 07/62] fix missing translation add material-icons font use
icon for all-day events
---
fonts/MaterialIcons/MaterialIcons.ttf | Bin 0 -> 356840 bytes
inkycal/custom/openweathermap_wrapper.py | 27 ++----
inkycal/modules/inkycal_agenda.py | 8 +-
inkycal/modules/inkycal_weather.py | 113 ++++++++++++-----------
tests/test_inkycal_agenda.py | 2 +-
tests/test_inkycal_weather.py | 2 +-
6 files changed, 75 insertions(+), 77 deletions(-)
create mode 100644 fonts/MaterialIcons/MaterialIcons.ttf
diff --git a/fonts/MaterialIcons/MaterialIcons.ttf b/fonts/MaterialIcons/MaterialIcons.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..9d09b0feb85c35beeaddd31246be0b7c8e0e69a4
GIT binary patch
literal 356840
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