Add support for longer update intervals

Load display-sizes from supported_models
Cleanup old images on new run of Inkycal
This commit is contained in:
Ace 2024-03-10 02:11:33 +01:00
parent 1b94162ac4
commit ae86daf6b8
5 changed files with 106 additions and 44 deletions

View File

@ -21,7 +21,7 @@ logs = logging.getLogger(__name__)
logs.setLevel(level=logging.INFO) logs.setLevel(level=logging.INFO)
# Get the path to the Inkycal folder # Get the path to the Inkycal folder
top_level = os.path.dirname(os.path.abspath(os.path.dirname(__file__))).split("/inkycal")[0] 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 # Get path of 'fonts' and 'images' folders within Inkycal folder
fonts_location = os.path.join(top_level, "fonts/") fonts_location = os.path.join(top_level, "fonts/")

View File

@ -2,15 +2,14 @@
Inkycal ePaper driving functions Inkycal ePaper driving functions
Copyright by aceisace Copyright by aceisace
""" """
import logging
import os import os
import traceback
from importlib import import_module from importlib import import_module
import PIL import PIL
from PIL import Image from PIL import Image
from inkycal.custom import top_level from inkycal.custom import top_level
from inkycal.display.supported_models import supported_models
def import_driver(model): def import_driver(model):
@ -47,14 +46,12 @@ class Display:
except FileNotFoundError: except FileNotFoundError:
raise Exception('SPI could not be found. Please check if SPI is enabled') raise Exception('SPI could not be found. Please check if SPI is enabled')
def test(self) -> None: def test(self) -> None:
"""Test the display by showing a test image""" """Test the display by showing a test image"""
# TODO implement test image # TODO implement test image
raise NotImplementedError("Devs were too lazy again, sorry, please try again later") raise NotImplementedError("Devs were too lazy again, sorry, please try again later")
def render(self, im_black: PIL.Image, im_colour: PIL.Image or None = None) -> None:
def render(self, im_black: PIL.Image, im_colour: PIL.Image or None=None) -> None:
"""Renders an image on the selected E-Paper display. """Renders an image on the selected E-Paper display.
Initlializes the E-Paper display, sends image data and executes command Initlializes the E-Paper display, sends image data and executes command
@ -166,26 +163,25 @@ class Display:
def get_display_size(cls, model_name) -> (int, int): def get_display_size(cls, model_name) -> (int, int):
"""Returns the size of the display as a tuple -> (width, height) """Returns the size of the display as a tuple -> (width, height)
Looks inside "drivers" folder for the given model name, then returns it's Looks inside supported_models file for the given model name, then returns it's
size. size.
Args: Args:
- model_name: str -> The name of the E-Paper display to get it's size. model_name: str -> The name of the E-Paper display to get it's size.
Returns: Returns:
(width, height) ->tuple, showing the size of the display (width, height) representing the size of the display
Raises:
AssertionError: If the display name was not found in the supported models.
You can use this function directly without creating the Display class: You can use this function directly without creating the Display class:
>>> Display.get_display_size('model_name') >>> Display.get_display_size('model_name')
""" """
try: if model_name in supported_models:
driver = import_driver(model_name) return supported_models[model_name]
return driver.EPD_WIDTH, driver.EPD_HEIGHT raise AssertionError(f'{model_name} not found in supported models')
except:
logging.error(f'Failed to load driver for ${model_name}. Check spelling?')
print(traceback.format_exc())
raise AssertionError("Could not import driver")
@classmethod @classmethod
def get_display_names(cls) -> list: def get_display_names(cls) -> list:

View File

@ -0,0 +1,19 @@
supported_models = {
'epd_12_in_48': (1304, 984),
'epd_7_in_5_colour': (640, 384),
'9_in_7': (1200, 825),
'epd_5_in_83_colour': (600, 448),
'epd_12_in_48_colour': (1304, 984),
'epd_4_in_2_colour': (400, 300),
'epd_7_in_5_v2': (800, 480),
'epd_12_in_48_colour_V2': (1304, 984),
'epd_7_in_5': (640, 384),
'epd5in83b_V2': (648, 480),
'epd_7_in_5_v3': (880, 528),
'10_in_3': (1872, 1404),
'epd_7_in_5_v2_colour': (800, 480),
'epd_4_in_2': (400, 300),
'7_in_8': (1872, 1404),
'epd_7_in_5_v3_colour': (880, 528),
'epd_5_in_83': (600, 448)
}

View File

@ -3,28 +3,22 @@ Main class for inkycal Project
Copyright by aceinnolab Copyright by aceinnolab
""" """
import asyncio
import glob import glob
import hashlib import hashlib
import json
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import arrow
import numpy import numpy
import asyncio
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 PIL import Image
# On the console, set a logger to show only important logs # On the console, set a logger to show only important logs
# (level ERROR or higher) # (level ERROR or higher)
stream_handler = logging.StreamHandler() stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.ERROR) stream_handler.setLevel(logging.ERROR)
if not os.path.exists(f'{top_level}/logs'): if not os.path.exists(f'{top_level}/logs'):
os.mkdir(f'{top_level}/logs') os.mkdir(f'{top_level}/logs')
@ -66,7 +60,7 @@ class Inkycal:
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper. to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
""" """
def __init__(self, settings_path:str or None=None, render:bool=True): def __init__(self, settings_path: str or None = None, render: bool = True):
"""Initialise Inkycal""" """Initialise Inkycal"""
# Get the release version from setup.py # Get the release version from setup.py
@ -87,7 +81,8 @@ class Inkycal:
self.settings = settings self.settings = settings
except FileNotFoundError: except FileNotFoundError:
raise FileNotFoundError(f"No settings.json file could be found in the specified location: {settings_path}") raise FileNotFoundError(
f"No settings.json file could be found in the specified location: {settings_path}")
else: else:
try: try:
@ -108,6 +103,8 @@ class Inkycal:
self.show_border = self.settings.get('border_around_modules', False) self.show_border = self.settings.get('border_around_modules', False)
self.cleanup()
# Load drivers if image should be rendered # Load drivers if image should be rendered
if self.render: if self.render:
# Init Display class with model in settings file # Init Display class with model in settings file
@ -146,7 +143,7 @@ class Inkycal:
logger.exception(f'Could not find module: "{module}". Please try to import manually') logger.exception(f'Could not find module: "{module}". Please try to import manually')
# If something unexpected happened, show the error message # If something unexpected happened, show the error message
except Exception as e: except:
logger.exception(f"Exception: {traceback.format_exc()}.") logger.exception(f"Exception: {traceback.format_exc()}.")
# Path to store images # Path to store images
@ -158,8 +155,16 @@ class Inkycal:
# Give an OK message # Give an OK message
print('loaded inkycal') print('loaded inkycal')
def countdown(self, interval_mins=None): def countdown(self, interval_mins: int or None = None) -> int:
"""Returns the remaining time in seconds until next display update""" """Returns the remaining time in seconds until next display update.
Args:
- interval_mins = int -> the interval in minutes for the update
if no interval is given, the value from the settings file is used.
Returns:
- int -> the remaining time in seconds until next update
"""
# Check if empty, if empty, use value from settings file # Check if empty, if empty, use value from settings file
if interval_mins is None: if interval_mins is None:
@ -167,20 +172,30 @@ class Inkycal:
# Find out at which minutes the update should happen # Find out at which minutes the update should happen
now = arrow.now() now = arrow.now()
update_timings = [(60 - int(interval_mins) * updates) for updates in if interval_mins <= 60:
range(60 // int(interval_mins))][::-1] update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1]
# Calculate time in minutes until next update # Calculate time in minutes until next update
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
# Print the remaining time in minutes until next update # Print the remaining time in minutes until next update
print(f'{minutes} minutes left until next refresh') print(f'{minutes} minutes left until next refresh')
# Calculate time in seconds until next update # Calculate time in seconds until next update
remaining_time = minutes * 60 + (60 - now.second) remaining_time = minutes * 60 + (60 - now.second)
# Return seconds until next update # Return seconds until next update
return remaining_time return remaining_time
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'{round(minutes / 60, 1)} hours left until next refresh')
# 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.
@ -262,7 +277,6 @@ 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):
"""Runs main program in nonstop mode. """Runs main program in nonstop mode.
@ -346,8 +360,8 @@ class Inkycal:
# 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 # render the image on the display
display.render(im_black, im_colour) display.render(im_black, im_colour)
@ -362,7 +376,7 @@ 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)
@ -557,6 +571,16 @@ class Inkycal:
else: else:
self._calibration_state = False self._calibration_state = False
@staticmethod
def cleanup():
# clean up old images in image_folder
for _file in glob.glob(f"{image_folder}*.png"):
try:
os.remove(_file)
except:
logger.error(f"could not remove file: {_file}")
pass
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

@ -17,12 +17,35 @@ class TestMain(unittest.TestCase):
assert inkycal.settings["model"] == "image_file" assert inkycal.settings["model"] == "image_file"
assert inkycal.settings["update_interval"] == 5 assert inkycal.settings["update_interval"] == 5
assert inkycal.settings["orientation"] == 0 assert inkycal.settings["orientation"] == 0
assert inkycal.settings["info_section"] == True assert inkycal.settings["info_section"] is True
assert inkycal.settings["info_section_height"] == 70 assert inkycal.settings["info_section_height"] == 70
assert inkycal.settings["border_around_modules"] == True assert inkycal.settings["border_around_modules"] is True
def test_run(self): def test_run(self):
inkycal = Inkycal(self.settings_path, render=False) inkycal = Inkycal(self.settings_path, render=False)
inkycal.test() inkycal.test()
def test_countdown(self):
inkycal = Inkycal(self.settings_path, render=False)
remaining_time = inkycal.countdown(5)
assert 1 <= remaining_time <= 5 * 60
remaining_time = inkycal.countdown(10)
assert 1 <= remaining_time <= 10 * 60
remaining_time = inkycal.countdown(15)
assert 1 <= remaining_time <= 15 * 60
remaining_time = inkycal.countdown(20)
assert 1 <= remaining_time <= 20 * 60
remaining_time = inkycal.countdown(30)
assert 1 <= remaining_time <= 30 * 60
remaining_time = inkycal.countdown(60)
assert 1 <= remaining_time <= 60 * 60
remaining_time = inkycal.countdown(120)
assert 1 <= remaining_time <= 120 * 2 * 60
remaining_time = inkycal.countdown(240)
assert 1 <= remaining_time <= 240 * 2 * 60
remaining_time = inkycal.countdown(600)
assert 1 <= remaining_time <= 600 * 2 * 60
remaining_time = inkycal.countdown(1200)
assert 1 <= remaining_time <= 1200 * 2 * 60