Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for longer update intervals #324

Merged
merged 1 commit into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion inkycal/custom/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
logs.setLevel(level=logging.INFO)

# 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
fonts_location = os.path.join(top_level, "fonts/")
Expand Down
26 changes: 11 additions & 15 deletions inkycal/display/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
Inkycal ePaper driving functions
Copyright by aceisace
"""
import logging
import os
import traceback
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


def import_driver(model):
Expand Down Expand Up @@ -47,14 +46,12 @@ def __init__(self, epaper_model):
except FileNotFoundError:
raise Exception('SPI could not be found. Please check if SPI is enabled')


def test(self) -> None:
"""Test the display by showing a test image"""
# TODO implement test image
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.

Initlializes the E-Paper display, sends image data and executes command
Expand Down Expand Up @@ -166,26 +163,25 @@ def calibrate(self, cycles=3):
def get_display_size(cls, model_name) -> (int, int):
"""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.

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:
(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:

>>> Display.get_display_size('model_name')
"""
try:
driver = import_driver(model_name)
return driver.EPD_WIDTH, driver.EPD_HEIGHT
except:
logging.error(f'Failed to load driver for ${model_name}. Check spelling?')
print(traceback.format_exc())
raise AssertionError("Could not import driver")
if model_name in supported_models:
return supported_models[model_name]
raise AssertionError(f'{model_name} not found in supported models')

@classmethod
def get_display_names(cls) -> list:
Expand Down
19 changes: 19 additions & 0 deletions inkycal/display/supported_models.py
Original file line number Diff line number Diff line change
@@ -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)
}
76 changes: 50 additions & 26 deletions inkycal/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,22 @@
Copyright by aceinnolab
"""

import asyncio
import glob
import hashlib
import json
from logging.handlers import RotatingFileHandler

import arrow
import numpy
import asyncio


from inkycal.custom import *
from inkycal.display import Display
from inkycal.modules.inky_image import Inkyimage as Images

from PIL import Image

# 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')

Expand Down Expand Up @@ -66,7 +60,7 @@ class Inkycal:
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
"""

def __init__(self, settings_path:str or None=None, render:bool=True):
def __init__(self, settings_path: str or None = None, render: bool = True):
"""Initialise Inkycal"""

# Get the release version from setup.py
Expand All @@ -87,7 +81,8 @@ def __init__(self, settings_path:str or None=None, render:bool=True):
self.settings = settings

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:
try:
Expand All @@ -108,6 +103,8 @@ def __init__(self, settings_path:str or None=None, render:bool=True):

self.show_border = self.settings.get('border_around_modules', False)

self.cleanup()

# Load drivers if image should be rendered
if self.render:
# Init Display class with model in settings file
Expand Down Expand Up @@ -146,7 +143,7 @@ def __init__(self, settings_path:str or None=None, render:bool=True):
logger.exception(f'Could not find module: "{module}". Please try to import manually')

# If something unexpected happened, show the error message
except Exception as e:
except:
logger.exception(f"Exception: {traceback.format_exc()}.")

# Path to store images
Expand All @@ -158,29 +155,47 @@ def __init__(self, settings_path:str or None=None, render:bool=True):
# Give an OK message
print('loaded inkycal')

def countdown(self, interval_mins=None):
"""Returns the remaining time in seconds until next display update"""
def countdown(self, interval_mins: int or None = None) -> int:
"""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
if interval_mins is None:
interval_mins = self.settings["update_interval"]

# Find out at which minutes the update should happen
now = arrow.now()
update_timings = [(60 - int(interval_mins) * updates) for updates in
range(60 // int(interval_mins))][::-1]
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 time in minutes until next update
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute

# Print the remaining time in minutes until next update
print(f'{minutes} minutes left until next refresh')
# Print the remaining time in minutes until next update
print(f'{minutes} minutes left until next refresh')

# Calculate time in seconds until next update
remaining_time = minutes * 60 + (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
# Return seconds until next update
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):
"""Tests if Inkycal can run without issues.
Expand Down Expand Up @@ -262,7 +277,6 @@ def _needs_image_update(self, _list):
print("Refresh needed: {a}".format(a=res))
return res


async def run(self):
"""Runs main program in nonstop mode.

Expand Down Expand Up @@ -346,8 +360,8 @@ async def run(self):

# 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)
(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)
Expand All @@ -362,7 +376,7 @@ async def run(self):
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)

Expand Down Expand Up @@ -557,6 +571,16 @@ def _calibration_check(self):
else:
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__':
print(f'running inkycal main in standalone/debug mode')
27 changes: 25 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,35 @@ def test_init(self):
assert inkycal.settings["model"] == "image_file"
assert inkycal.settings["update_interval"] == 5
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["border_around_modules"] == True
assert inkycal.settings["border_around_modules"] is True

def test_run(self):
inkycal = Inkycal(self.settings_path, render=False)
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