@ -1,12 +0,0 @@
|
|||||||
"""
|
|
||||||
Clears the display of any content.
|
|
||||||
"""
|
|
||||||
from inkycal import Inkycal
|
|
||||||
|
|
||||||
print("loading Inkycal and display driver...")
|
|
||||||
inky = Inkycal(render=True) # Initialise Inkycal
|
|
||||||
print("clearing display...")
|
|
||||||
inky.calibrate(cycles=1) # Calibrate the display
|
|
||||||
print("clear complete...")
|
|
||||||
|
|
||||||
print("finished!")
|
|
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 |
41
inky_run.py
@ -1,7 +1,40 @@
|
|||||||
|
"""Basic Inkycal run script.
|
||||||
|
|
||||||
|
Assumes that the settings.json file is in the /boot directory.
|
||||||
|
set render=True to render the display, set render=False to only run the modules.
|
||||||
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from inkycal import Inkycal
|
from inkycal import Inkycal
|
||||||
|
|
||||||
inky = Inkycal(render=True) # Initialise Inkycal
|
|
||||||
# If your settings.json file is not in /boot, use the full path: inky = Inkycal('path/to/settings.json', render=True)
|
async def dry_run():
|
||||||
inky.test() # test if Inkycal can be run correctly, running this will show a bit of info for each module
|
# create an instance of Inkycal
|
||||||
asyncio.run(inky.run()) # If there were no issues, you can run Inkycal nonstop
|
# If your settings.json file is not in /boot, use the full path:
|
||||||
|
# inky = Inkycal('path/to/settings.json', render=True)
|
||||||
|
inky = Inkycal(render=False)
|
||||||
|
await inky.run(run_once=True) # dry-run without rendering anything on the display
|
||||||
|
|
||||||
|
|
||||||
|
async def clear_display():
|
||||||
|
print("loading Inkycal and display driver...")
|
||||||
|
inky = Inkycal(render=True) # Initialise Inkycal
|
||||||
|
print("clearing display...")
|
||||||
|
inky.calibrate(cycles=1) # Calibrate the display
|
||||||
|
print("clear complete...")
|
||||||
|
print("finished!")
|
||||||
|
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
# create an instance of Inkycal
|
||||||
|
# If your settings.json file is not in /boot, use the full path:
|
||||||
|
# inky = Inkycal('path/to/settings.json', render=True)
|
||||||
|
|
||||||
|
# when using experimental PiSugar support:
|
||||||
|
# inky = Inkycal(render=True, use_pi_sugar=True)
|
||||||
|
inky = Inkycal(render=True)
|
||||||
|
await inky.run() # If there were no issues, you can run Inkycal nonstop
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run())
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
# Display class (for driving E-Paper displays)
|
|
||||||
from inkycal.display import Display
|
|
||||||
|
|
||||||
# Default modules
|
# Default modules
|
||||||
import inkycal.modules.inkycal_agenda
|
import inkycal.modules.inkycal_agenda
|
||||||
import inkycal.modules.inkycal_calendar
|
import inkycal.modules.inkycal_calendar
|
||||||
import inkycal.modules.inkycal_weather
|
|
||||||
import inkycal.modules.inkycal_feeds
|
import inkycal.modules.inkycal_feeds
|
||||||
import inkycal.modules.inkycal_todoist
|
import inkycal.modules.inkycal_fullweather
|
||||||
import inkycal.modules.inkycal_image
|
import inkycal.modules.inkycal_image
|
||||||
import inkycal.modules.inkycal_jokes
|
import inkycal.modules.inkycal_jokes
|
||||||
import inkycal.modules.inkycal_slideshow
|
import inkycal.modules.inkycal_slideshow
|
||||||
import inkycal.modules.inkycal_stocks
|
import inkycal.modules.inkycal_stocks
|
||||||
|
import inkycal.modules.inkycal_todoist
|
||||||
|
import inkycal.modules.inkycal_weather
|
||||||
import inkycal.modules.inkycal_webshot
|
import inkycal.modules.inkycal_webshot
|
||||||
import inkycal.modules.inkycal_xkcd
|
import inkycal.modules.inkycal_xkcd
|
||||||
import inkycal.modules.inkycal_fullweather
|
from inkycal.display import Display
|
||||||
|
|
||||||
# Main file
|
|
||||||
from inkycal.main import Inkycal
|
from inkycal.main import Inkycal
|
||||||
import inkycal.modules.inkycal_stocks
|
|
||||||
|
@ -17,20 +17,16 @@ from PIL import Image
|
|||||||
from PIL import ImageDraw
|
from PIL import ImageDraw
|
||||||
from PIL import ImageFont
|
from PIL import ImageFont
|
||||||
|
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(level=logging.INFO)
|
|
||||||
|
|
||||||
# Get the path to the Inkycal folder
|
settings = Settings()
|
||||||
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/")
|
|
||||||
image_folder = os.path.join(top_level, "image_folder/")
|
|
||||||
|
|
||||||
# Get available fonts within fonts folder
|
# Get available fonts within fonts folder
|
||||||
fonts = {}
|
fonts = {}
|
||||||
|
|
||||||
for path, dirs, files in os.walk(fonts_location):
|
for path, dirs, files in os.walk(settings.FONT_PATH):
|
||||||
for _ in files:
|
for _ in files:
|
||||||
if _.endswith(".otf"):
|
if _.endswith(".otf"):
|
||||||
name = _.split(".otf")[0]
|
name = _.split(".otf")[0]
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
Inkycal ePaper driving functions
|
Inkycal ePaper driving functions
|
||||||
Copyright by aceisace
|
Copyright by aceisace
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
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.display.supported_models import supported_models
|
from inkycal.display.supported_models import supported_models
|
||||||
|
|
||||||
|
|
||||||
@ -199,9 +197,7 @@ class Display:
|
|||||||
|
|
||||||
>>> Display.get_display_names()
|
>>> Display.get_display_names()
|
||||||
"""
|
"""
|
||||||
driver_files = top_level + '/inkycal/display/drivers/'
|
return list(supported_models.keys())
|
||||||
drivers = [i for i in os.listdir(driver_files) if i.endswith(".py") and not i.startswith("__") and "_" in i]
|
|
||||||
return drivers
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -2,22 +2,18 @@
|
|||||||
10.3" driver class
|
10.3" driver class
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from inkycal.custom import image_folder, top_level
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
# Display resolution
|
# Display resolution
|
||||||
EPD_WIDTH = 1872
|
EPD_WIDTH = 1872
|
||||||
EPD_HEIGHT = 1404
|
EPD_HEIGHT = 1404
|
||||||
|
|
||||||
# Please insert VCOM of your display. The Minus sign before is not required
|
settings = Settings()
|
||||||
VCOM = "2.0"
|
|
||||||
|
|
||||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
|
||||||
|
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
|
||||||
|
|
||||||
|
|
||||||
class EPD:
|
class EPD:
|
||||||
@ -40,8 +36,8 @@ class EPD:
|
|||||||
def getbuffer(self, image):
|
def getbuffer(self, image):
|
||||||
"""ad-hoc"""
|
"""ad-hoc"""
|
||||||
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
image = image.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
|
||||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP')
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
|
||||||
print(command)
|
print(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
@ -2,20 +2,16 @@
|
|||||||
7.8" parallel driver class
|
7.8" parallel driver class
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
|
||||||
from inkycal.custom import image_folder, top_level
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
# Display resolution
|
# Display resolution
|
||||||
EPD_WIDTH = 1872
|
EPD_WIDTH = 1872
|
||||||
EPD_HEIGHT = 1404
|
EPD_HEIGHT = 1404
|
||||||
|
|
||||||
# Please insert VCOM of your display. The Minus sign before is not required
|
settings = Settings()
|
||||||
VCOM = "2.0"
|
|
||||||
|
|
||||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
|
||||||
|
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
|
||||||
|
|
||||||
|
|
||||||
class EPD:
|
class EPD:
|
||||||
@ -38,8 +34,8 @@ class EPD:
|
|||||||
def getbuffer(self, image):
|
def getbuffer(self, image):
|
||||||
"""ad-hoc"""
|
"""ad-hoc"""
|
||||||
image = image.rotate(90, expand=True)
|
image = image.rotate(90, expand=True)
|
||||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
image.convert('RGB').save(os.path.join(settings.IMAGE_FOLDER, 'canvas.bmp'), 'BMP')
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
|
||||||
print(command)
|
print(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
@ -4,18 +4,16 @@ Copyright by aceinnolab
|
|||||||
"""
|
"""
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
|
||||||
from inkycal.custom import image_folder, top_level
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
# Display resolution
|
# Display resolution
|
||||||
EPD_WIDTH = 1200
|
EPD_WIDTH = 1200
|
||||||
EPD_HEIGHT = 825
|
EPD_HEIGHT = 825
|
||||||
|
|
||||||
# Please insert VCOM of your display. The Minus sign before is not required
|
|
||||||
VCOM = "2.0"
|
|
||||||
|
|
||||||
driver_dir = top_level + '/inkycal/display/drivers/parallel_drivers/'
|
settings = Settings()
|
||||||
|
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
|
||||||
|
|
||||||
|
|
||||||
class EPD:
|
class EPD:
|
||||||
@ -38,8 +36,8 @@ class EPD:
|
|||||||
def getbuffer(self, image):
|
def getbuffer(self, image):
|
||||||
"""ad-hoc"""
|
"""ad-hoc"""
|
||||||
image = image.rotate(90, expand=True)
|
image = image.rotate(90, expand=True)
|
||||||
image.convert('RGB').save(image_folder + 'canvas.bmp', 'BMP')
|
image.convert('RGB').save(settings.IMAGE_FOLDER + 'canvas.bmp', 'BMP')
|
||||||
command = f'sudo {driver_dir}epd -{VCOM} 0 {image_folder + "canvas.bmp"}'
|
command = f'sudo {settings.PARALLEL_DRIVER_PATH}/epd -{settings.VCOM} 0 {settings.IMAGE_FOLDER + "canvas.bmp"}'
|
||||||
print(command)
|
print(command)
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
@ -28,8 +28,6 @@ THE SOFTWARE.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -128,4 +126,3 @@ implementation = RaspberryPi()
|
|||||||
|
|
||||||
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
||||||
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
||||||
|
|
||||||
|
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.INFO)
|
||||||
|
|
||||||
|
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)
|
311
inkycal/main.py
@ -6,44 +6,21 @@ Copyright by aceinnolab
|
|||||||
import asyncio
|
import asyncio
|
||||||
import glob
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
from inkycal import loggers # noqa
|
||||||
from inkycal.custom import *
|
from inkycal.custom import *
|
||||||
from inkycal.display import Display
|
from inkycal.display import Display
|
||||||
from inkycal.modules.inky_image import Inkyimage as Images
|
from inkycal.modules.inky_image import Inkyimage as Images
|
||||||
|
from inkycal.utils import JSONCache
|
||||||
# On the console, set a logger to show only important logs
|
|
||||||
# (level ERROR or higher)
|
|
||||||
stream_handler = logging.StreamHandler()
|
|
||||||
stream_handler.setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
if not os.path.exists(f'{top_level}/logs'):
|
|
||||||
os.mkdir(f'{top_level}/logs')
|
|
||||||
|
|
||||||
# Save all logs to a file, which contains more detailed output
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
|
||||||
datefmt='%d-%m-%Y %H:%M:%S',
|
|
||||||
handlers=[
|
|
||||||
stream_handler, # add stream handler from above
|
|
||||||
RotatingFileHandler( # log to a file too
|
|
||||||
f'{top_level}/logs/inkycal.log', # file to log
|
|
||||||
maxBytes=2097152, # 2MB max filesize
|
|
||||||
backupCount=5 # create max 5 log files
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Show less logging for PIL module
|
|
||||||
logging.getLogger("PIL").setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
CACHE_NAME = "inkycal_main"
|
||||||
|
|
||||||
# TODO: autostart -> supervisor?
|
|
||||||
|
|
||||||
class Inkycal:
|
class Inkycal:
|
||||||
"""Inkycal main class
|
"""Inkycal main class
|
||||||
@ -60,43 +37,45 @@ 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, use_pi_sugar: bool = False):
|
||||||
"""Initialise Inkycal"""
|
"""Initialise Inkycal"""
|
||||||
|
self._release = "2.0.3"
|
||||||
|
|
||||||
# Get the release version from setup.py
|
logger.info(f"Inkycal v{self._release} booting up...")
|
||||||
with open(f'{top_level}/setup.py') as setup_file:
|
|
||||||
for line in setup_file:
|
|
||||||
if line.startswith('__version__'):
|
|
||||||
self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "")
|
|
||||||
break
|
|
||||||
|
|
||||||
self.render = render
|
self.render = render
|
||||||
self.info = None
|
self.info = None
|
||||||
|
|
||||||
|
logger.info("Checking if a settings file is present...")
|
||||||
# load settings file - throw an error if file could not be found
|
# load settings file - throw an error if file could not be found
|
||||||
if settings_path:
|
if settings_path:
|
||||||
|
logger.info(f"Custom location for settings.json file specified: {settings_path}")
|
||||||
try:
|
try:
|
||||||
with open(settings_path) as settings_file:
|
with open(settings_path) as settings_file:
|
||||||
settings = json.load(settings_file)
|
self.settings = json.load(settings_file)
|
||||||
self.settings = settings
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"No settings.json file could be found in the specified location: {settings_path}")
|
f"No settings.json file could be found in the specified location: {settings_path}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
logger.info("Looking for settings.json file in /boot folder...")
|
||||||
try:
|
try:
|
||||||
with open('/boot/settings.json') as settings_file:
|
with open('/boot/settings.json', mode="r") as settings_file:
|
||||||
settings = json.load(settings_file)
|
self.settings = json.load(settings_file)
|
||||||
self.settings = settings
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise SettingsFileNotFoundError
|
raise SettingsFileNotFoundError
|
||||||
|
|
||||||
self.disable_calibration = self.settings.get('disable_calibration', False)
|
self.disable_calibration = self.settings.get('disable_calibration', False)
|
||||||
|
if self.disable_calibration:
|
||||||
|
logger.info("Calibration disabled. Please proceed with caution to prevent ghosting.")
|
||||||
|
|
||||||
if not os.path.exists(image_folder):
|
if not os.path.exists(settings.IMAGE_FOLDER):
|
||||||
os.mkdir(image_folder)
|
os.mkdir(settings.IMAGE_FOLDER)
|
||||||
|
|
||||||
|
if not os.path.exists(settings.CACHE_PATH):
|
||||||
|
os.mkdir(settings.CACHE_PATH)
|
||||||
|
|
||||||
# Option to use epaper image optimisation, reduces colours
|
# Option to use epaper image optimisation, reduces colours
|
||||||
self.optimize = True
|
self.optimize = True
|
||||||
@ -109,10 +88,10 @@ class Inkycal:
|
|||||||
if self.render:
|
if self.render:
|
||||||
# Init Display class with model in settings file
|
# Init Display class with model in settings file
|
||||||
# from inkycal.display import Display
|
# from inkycal.display import Display
|
||||||
self.Display = Display(settings["model"])
|
self.Display = Display(self.settings["model"])
|
||||||
|
|
||||||
# check if colours can be rendered
|
# check if colours can be rendered
|
||||||
self.supports_colour = True if 'colour' in settings['model'] else False
|
self.supports_colour = True if 'colour' in self.settings['model'] else False
|
||||||
|
|
||||||
# get calibration hours
|
# get calibration hours
|
||||||
self._calibration_hours = self.settings['calibration_hours']
|
self._calibration_hours = self.settings['calibration_hours']
|
||||||
@ -122,7 +101,7 @@ class Inkycal:
|
|||||||
|
|
||||||
# Load and initialise modules specified in the settings file
|
# Load and initialise modules specified in the settings file
|
||||||
self._module_number = 1
|
self._module_number = 1
|
||||||
for module in settings['modules']:
|
for module in self.settings['modules']:
|
||||||
module_name = module['name']
|
module_name = module['name']
|
||||||
try:
|
try:
|
||||||
loader = f'from inkycal.modules import {module_name}'
|
loader = f'from inkycal.modules import {module_name}'
|
||||||
@ -131,10 +110,9 @@ class Inkycal:
|
|||||||
setup = f'self.module_{self._module_number} = {module_name}({module})'
|
setup = f'self.module_{self._module_number} = {module_name}({module})'
|
||||||
# print(setup)
|
# print(setup)
|
||||||
exec(setup)
|
exec(setup)
|
||||||
logger.info(('name : {name} size : {width}x{height} px'.format(
|
width = module['config']['size'][0]
|
||||||
name=module_name,
|
height = module['config']['size'][1]
|
||||||
width=module['config']['size'][0],
|
logger.info(f'name : {module_name} size : {width}x{height} px')
|
||||||
height=module['config']['size'][1])))
|
|
||||||
|
|
||||||
self._module_number += 1
|
self._module_number += 1
|
||||||
|
|
||||||
@ -147,57 +125,83 @@ class Inkycal:
|
|||||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
logger.exception(f"Exception: {traceback.format_exc()}.")
|
||||||
|
|
||||||
# Path to store images
|
# Path to store images
|
||||||
self.image_folder = image_folder
|
self.image_folder = settings.IMAGE_FOLDER
|
||||||
|
|
||||||
# Remove old hashes
|
# Remove old hashes
|
||||||
self._remove_hashes(self.image_folder)
|
self._remove_hashes(self.image_folder)
|
||||||
|
|
||||||
# Give an OK message
|
# set up cache
|
||||||
print('loaded inkycal')
|
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()
|
||||||
|
|
||||||
def countdown(self, interval_mins: int or None = None) -> int:
|
self.counter = 0 if "counter" not in self.cache_data else int(self.cache_data["counter"])
|
||||||
"""Returns the remaining time in seconds until next display update.
|
|
||||||
|
self.use_pi_sugar = use_pi_sugar
|
||||||
|
self.battery_capacity = 100
|
||||||
|
|
||||||
|
if self.use_pi_sugar:
|
||||||
|
logger.info("PiSugar support enabled.")
|
||||||
|
from inkycal.utils import PiSugar
|
||||||
|
self.pisugar = PiSugar()
|
||||||
|
|
||||||
|
self.battery_capacity = self.pisugar.get_battery()
|
||||||
|
logger.info(f"PiSugar battery capacity: {self.battery_capacity}%")
|
||||||
|
|
||||||
|
if self.battery_capacity < 20:
|
||||||
|
logger.warning("Battery capacity is below 20%!")
|
||||||
|
|
||||||
|
logger.info("Setting system time to PiSugar time...")
|
||||||
|
if self.pisugar.rtc_pi2rtc():
|
||||||
|
logger.info("RTC time updates successfully")
|
||||||
|
else:
|
||||||
|
logger.warning("RTC time could not be set!")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Using PiSigar model: {self.pisugar.get_model()}. Current PiSugar time: {self.pisugar.get_rtc_time()}")
|
||||||
|
|
||||||
|
# Give an OK message
|
||||||
|
logger.info('Inkycal initialised successfully!')
|
||||||
|
|
||||||
|
def countdown(self, interval_mins: int = None) -> int:
|
||||||
|
"""Returns the remaining time in seconds until the next display update based on the interval.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- interval_mins = int -> the interval in minutes for the update
|
interval_mins (int): The interval in minutes for the update. If none is given, the value
|
||||||
if no interval is given, the value from the settings file is used.
|
from the settings file is used.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- int -> the remaining time in seconds until next update
|
int: The remaining time in seconds until the next update.
|
||||||
"""
|
"""
|
||||||
|
# Default to settings if no interval is provided
|
||||||
# Check if empty, if empty, use value from settings file
|
|
||||||
if interval_mins is None:
|
if interval_mins is None:
|
||||||
interval_mins = self.settings["update_interval"]
|
interval_mins = self.settings["update_interval"]
|
||||||
|
|
||||||
# Find out at which minutes the update should happen
|
# Get the current time
|
||||||
now = arrow.now()
|
now = arrow.now()
|
||||||
if interval_mins <= 60:
|
|
||||||
update_timings = [(60 - interval_mins * updates) for updates in range(60 // interval_mins)][::-1]
|
|
||||||
|
|
||||||
# Calculate time in minutes until next update
|
# Calculate the next update time
|
||||||
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
|
# Finding the total minutes from the start of the day
|
||||||
|
minutes_since_midnight = now.hour * 60 + now.minute
|
||||||
|
|
||||||
# Print the remaining time in minutes until next update
|
# Finding the next interval point
|
||||||
print(f'{minutes} minutes left until next refresh')
|
minutes_to_next_interval = (
|
||||||
|
minutes_since_midnight // interval_mins + 1) * interval_mins - minutes_since_midnight
|
||||||
|
seconds_to_next_interval = minutes_to_next_interval * 60 - now.second
|
||||||
|
|
||||||
# Calculate time in seconds until next update
|
# Logging the remaining time in appropriate units
|
||||||
remaining_time = minutes * 60 + (60 - now.second)
|
hours_to_next_interval = minutes_to_next_interval // 60
|
||||||
|
remaining_minutes = minutes_to_next_interval % 60
|
||||||
# Return seconds until next update
|
if hours_to_next_interval > 0:
|
||||||
return remaining_time
|
print(f'{hours_to_next_interval} hours and {remaining_minutes} minutes left until next refresh')
|
||||||
else:
|
else:
|
||||||
# Calculate time in minutes until next update using the range of 24 hours in steps of every full hour
|
print(f'{remaining_minutes} minutes left until next refresh')
|
||||||
update_timings = [(60 * 24 - interval_mins * updates) for updates in range(60 * 24 // interval_mins)][::-1]
|
|
||||||
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
|
|
||||||
remaining_time = minutes * 60 + (60 - now.second)
|
|
||||||
|
|
||||||
print(f'{round(minutes / 60, 1)} hours left until next refresh')
|
return seconds_to_next_interval
|
||||||
|
|
||||||
# Return seconds until next update
|
def dry_run(self):
|
||||||
return remaining_time
|
|
||||||
|
|
||||||
def test(self):
|
|
||||||
"""Tests if Inkycal can run without issues.
|
"""Tests if Inkycal can run without issues.
|
||||||
|
|
||||||
Attempts to import module names from settings file. Loads the config
|
Attempts to import module names from settings file. Loads the config
|
||||||
@ -206,8 +210,6 @@ class Inkycal:
|
|||||||
|
|
||||||
Generated images can be found in the /images folder of Inkycal.
|
Generated images can be found in the /images folder of Inkycal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(f"Inkycal version: v{self._release}")
|
|
||||||
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
||||||
|
|
||||||
# store module numbers in here
|
# store module numbers in here
|
||||||
@ -218,20 +220,13 @@ class Inkycal:
|
|||||||
|
|
||||||
for number in range(1, self._module_number):
|
for number in range(1, self._module_number):
|
||||||
name = eval(f"self.module_{number}.name")
|
name = eval(f"self.module_{number}.name")
|
||||||
module = eval(f'self.module_{number}')
|
success = self.process_module(number)
|
||||||
print(f'generating image(s) for {name}...', end="")
|
if success:
|
||||||
try:
|
logger.debug(f'Image of module {name} generated successfully')
|
||||||
black, colour = module.generate_image()
|
else:
|
||||||
if self.show_border:
|
logger.warning(f'Generating image of module {name} failed!')
|
||||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
|
||||||
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
|
||||||
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
|
||||||
print("OK!")
|
|
||||||
except Exception:
|
|
||||||
errors.append(number)
|
errors.append(number)
|
||||||
self.info += f"module {number}: Error! "
|
self.info += f"module {number}: Error! "
|
||||||
logger.exception("Error!")
|
|
||||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
logger.error('Error/s in modules:', *errors)
|
logger.error('Error/s in modules:', *errors)
|
||||||
@ -277,76 +272,69 @@ class Inkycal:
|
|||||||
print("Refresh needed: {a}".format(a=res))
|
print("Refresh needed: {a}".format(a=res))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def run(self):
|
async def run(self, run_once=False):
|
||||||
"""Runs main program in nonstop mode.
|
"""Runs main program in nonstop mode or a single iteration based on the run_once flag.
|
||||||
|
|
||||||
Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
|
Args:
|
||||||
from all modules, assembles them in one image, refreshed the E-Paper and
|
run_once (bool): If True, runs the updating process once and stops. If False,
|
||||||
then sleeps until the next scheduled update.
|
runs indefinitely.
|
||||||
|
|
||||||
|
Uses an infinity loop to run Inkycal nonstop or a single time based on run_once.
|
||||||
|
Inkycal generates the image from all modules, assembles them in one image,
|
||||||
|
refreshes the E-Paper and then sleeps until the next scheduled update or exits.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Get the time of initial run
|
# Get the time of initial run
|
||||||
runtime = arrow.now()
|
runtime = arrow.now()
|
||||||
|
|
||||||
# Function to flip images upside down
|
# Function to flip images upside down
|
||||||
upside_down = lambda image: image.rotate(180, expand=True)
|
upside_down = lambda image: image.rotate(180, expand=True)
|
||||||
|
|
||||||
# Count the number of times without any errors
|
logger.info(f'Inkycal version: v{self._release}')
|
||||||
counter = 0
|
logger.info(f'Selected E-paper display: {self.settings["model"]}')
|
||||||
|
|
||||||
print(f'Inkycal version: v{self._release}')
|
|
||||||
print(f'Selected E-paper display: {self.settings["model"]}')
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
logger.info("Starting new cycle...")
|
||||||
current_time = arrow.now(tz=get_system_tz())
|
current_time = arrow.now(tz=get_system_tz())
|
||||||
print(f"Date: {current_time.format('D MMM YY')} | "
|
logger.info(f"Timestamp: {current_time.format('HH:mm:ss DD.MM.YYYY')}")
|
||||||
f"Time: {current_time.format('HH:mm')}")
|
self.cache_data["counter"] = self.counter
|
||||||
print('Generating images for all modules...', end='')
|
|
||||||
|
|
||||||
errors = [] # store module numbers in here
|
errors = [] # Store module numbers in here
|
||||||
|
|
||||||
# short info for info-section
|
# Short info for info-section
|
||||||
if not self.settings.get('image_hash', False):
|
if not self.settings.get('image_hash', False):
|
||||||
self.info = f"{current_time.format('D MMM @ HH:mm')} "
|
self.info = f"{current_time.format('D MMM @ HH:mm')} "
|
||||||
else:
|
else:
|
||||||
self.info = ""
|
self.info = ""
|
||||||
|
|
||||||
for number in range(1, self._module_number):
|
for number in range(1, self._module_number):
|
||||||
|
success = self.process_module(number)
|
||||||
# name = eval(f"self.module_{number}.name")
|
if not success:
|
||||||
module = eval(f'self.module_{number}')
|
|
||||||
|
|
||||||
try:
|
|
||||||
black, colour = module.generate_image()
|
|
||||||
if self.show_border:
|
|
||||||
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
|
||||||
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
|
||||||
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
|
||||||
self.info += f"module {number}: OK "
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(number)
|
errors.append(number)
|
||||||
self.info += f"module {number}: Error! "
|
self.info += f"im {number}: X "
|
||||||
logger.exception("Error!")
|
|
||||||
logger.exception(f"Exception: {traceback.format_exc()}.")
|
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
logger.error("Error/s in modules:", *errors)
|
logger.error("Error/s in modules:", *errors)
|
||||||
counter = 0
|
self.counter = 0
|
||||||
|
self.cache_data["counter"] = 0
|
||||||
else:
|
else:
|
||||||
counter += 1
|
self.counter += 1
|
||||||
logger.info("successful")
|
self.cache_data["counter"] += 1
|
||||||
|
logger.info("All images generated successfully!")
|
||||||
del errors
|
del errors
|
||||||
|
|
||||||
|
if self.battery_capacity < 20:
|
||||||
|
self.info += "Low battery! "
|
||||||
|
|
||||||
# Assemble image from each module - add info section if specified
|
# Assemble image from each module - add info section if specified
|
||||||
self._assemble()
|
self._assemble()
|
||||||
|
|
||||||
# Check if image should be rendered
|
# Check if image should be rendered
|
||||||
if self.render:
|
if self.render:
|
||||||
|
logger.info("Attempting to render image on display...")
|
||||||
display = self.Display
|
display = self.Display
|
||||||
|
|
||||||
self._calibration_check()
|
self._calibration_check()
|
||||||
if self._calibration_state:
|
if self._calibration_state:
|
||||||
# after calibration, we have to forcefully rewrite the screen
|
# After calibration, we have to forcefully rewrite the screen
|
||||||
self._remove_hashes(self.image_folder)
|
self._remove_hashes(self.image_folder)
|
||||||
|
|
||||||
if self.supports_colour:
|
if self.supports_colour:
|
||||||
@ -358,17 +346,15 @@ class Inkycal:
|
|||||||
im_black = upside_down(im_black)
|
im_black = upside_down(im_black)
|
||||||
im_colour = upside_down(im_colour)
|
im_colour = upside_down(im_colour)
|
||||||
|
|
||||||
# render the image on the display
|
# Render the image on the display
|
||||||
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
||||||
(f"{self.image_folder}/canvas.png.hash", im_black),
|
(f"{self.image_folder}/canvas.png.hash", im_black),
|
||||||
(f"{self.image_folder}/canvas_colour.png.hash", im_colour)
|
(f"{self.image_folder}/canvas_colour.png.hash", im_colour)
|
||||||
]):
|
]):
|
||||||
# render the image on the display
|
|
||||||
display.render(im_black, im_colour)
|
display.render(im_black, im_colour)
|
||||||
|
|
||||||
# Part for black-white ePapers
|
# Part for black-white ePapers
|
||||||
elif not self.supports_colour:
|
else:
|
||||||
|
|
||||||
im_black = self._merge_bands()
|
im_black = self._merge_bands()
|
||||||
|
|
||||||
# Flip the image by 180° if required
|
# Flip the image by 180° if required
|
||||||
@ -376,14 +362,29 @@ class Inkycal:
|
|||||||
im_black = upside_down(im_black)
|
im_black = upside_down(im_black)
|
||||||
|
|
||||||
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
if not self.settings.get('image_hash', False) or self._needs_image_update([
|
||||||
(f"{self.image_folder}/canvas.png.hash", im_black),
|
(f"{self.image_folder}/canvas.png.hash", im_black), ]):
|
||||||
]):
|
|
||||||
display.render(im_black)
|
display.render(im_black)
|
||||||
|
|
||||||
print(f'\nNo errors since {counter} display updates \n'
|
logger.info(f'\nNo errors since {self.counter} display updates')
|
||||||
f'program started {runtime.humanize()}')
|
logger.info(f'program started {runtime.humanize()}')
|
||||||
|
|
||||||
|
# store the cache data
|
||||||
|
self.cache.write(self.cache_data)
|
||||||
|
|
||||||
|
# Exit the loop if run_once is True
|
||||||
|
if run_once:
|
||||||
|
break # Exit the loop after one full cycle if run_once is True
|
||||||
|
|
||||||
sleep_time = self.countdown()
|
sleep_time = self.countdown()
|
||||||
|
|
||||||
|
if self.use_pi_sugar:
|
||||||
|
sleep_time_rtc = arrow.now(tz=get_system_tz()).shift(seconds=sleep_time)
|
||||||
|
result = self.pisugar.rtc_alarm_set(sleep_time_rtc, 127)
|
||||||
|
if result:
|
||||||
|
logger.info(f"Alarm set for {sleep_time_rtc.format('HH:mm:ss')}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to set alarm for {sleep_time_rtc.format('HH:mm:ss')}")
|
||||||
|
|
||||||
await asyncio.sleep(sleep_time)
|
await asyncio.sleep(sleep_time)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -392,7 +393,8 @@ class Inkycal:
|
|||||||
returns the merged image
|
returns the merged image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
im1_path, im2_path = image_folder + 'canvas.png', image_folder + 'canvas_colour.png'
|
im1_path = os.path.join(settings.image_folder, "canvas.png")
|
||||||
|
im2_path = os.path.join(settings.image_folder, "canvas_colour.png")
|
||||||
|
|
||||||
# If there is an image for black and colour, merge them
|
# If there is an image for black and colour, merge them
|
||||||
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
||||||
@ -531,7 +533,7 @@ class Inkycal:
|
|||||||
im_colour = black_to_colour(im_colour)
|
im_colour = black_to_colour(im_colour)
|
||||||
|
|
||||||
im_colour.paste(im_black, (0, 0), im_black)
|
im_colour.paste(im_black, (0, 0), im_black)
|
||||||
im_colour.save(image_folder + 'full-screen.png', 'PNG')
|
im_colour.save(os.path.join(settings.IMAGE_FOLDER, 'full-screen.png'), 'PNG')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _optimize_im(image, threshold=220):
|
def _optimize_im(image, threshold=220):
|
||||||
@ -574,13 +576,40 @@ class Inkycal:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanup():
|
def cleanup():
|
||||||
# clean up old images in image_folder
|
# clean up old images in image_folder
|
||||||
for _file in glob.glob(f"{image_folder}*.png"):
|
if len(glob.glob(settings.IMAGE_FOLDER)) <= 1:
|
||||||
|
return
|
||||||
|
for _file in glob.glob(settings.IMAGE_FOLDER):
|
||||||
try:
|
try:
|
||||||
os.remove(_file)
|
os.remove(_file)
|
||||||
except:
|
except:
|
||||||
logger.error(f"could not remove file: {_file}")
|
logger.error(f"could not remove file: {_file}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def process_module(self, number) -> bool or Exception:
|
||||||
|
"""Process individual module to generate images and handle exceptions."""
|
||||||
|
module = eval(f'self.module_{number}')
|
||||||
|
try:
|
||||||
|
black, colour = module.generate_image()
|
||||||
|
if self.show_border:
|
||||||
|
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
|
||||||
|
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
|
||||||
|
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Error in module {number}!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _shutdown_system(self):
|
||||||
|
"""Shutdown the system"""
|
||||||
|
import subprocess
|
||||||
|
from time import sleep
|
||||||
|
try:
|
||||||
|
logger.info("Shutting down OS in 5 seconds...")
|
||||||
|
sleep(5)
|
||||||
|
subprocess.run(["sudo", "shutdown", "-h", "now"], check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logger.warning("Failed to execute shutdown command.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print(f'running inkycal main in standalone/debug mode')
|
print(f'running inkycal main in standalone/debug mode')
|
||||||
|
@ -156,7 +156,7 @@ class Simple(inkycal_module):
|
|||||||
# -----------------------------------------------------------------------#
|
# -----------------------------------------------------------------------#
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
# Validation of module specific parameters (optional) #
|
# Validation of module specific parameters (optional) #
|
||||||
|
@ -27,7 +27,7 @@ class Inkyimage:
|
|||||||
self.image = image
|
self.image = image
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
logger.info(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def load(self, path: str) -> None:
|
def load(self, path: str) -> None:
|
||||||
"""loads an image from a URL or filepath.
|
"""loads an image from a URL or filepath.
|
||||||
@ -59,7 +59,7 @@ class Inkyimage:
|
|||||||
logger.error("Invalid Image file provided", exc_info=True)
|
logger.error("Invalid Image file provided", exc_info=True)
|
||||||
raise Exception("Please check if the path points to an image file.")
|
raise Exception("Please check if the path points to an image file.")
|
||||||
|
|
||||||
logger.info(f"width: {image.width}, height: {image.height}")
|
logger.debug(f"width: {image.width}, height: {image.height}")
|
||||||
|
|
||||||
image.convert(mode="RGBA") # convert to a more suitable format
|
image.convert(mode="RGBA") # convert to a more suitable format
|
||||||
self.image = image
|
self.image = image
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
Inkycal Agenda Module
|
Inkycal Agenda Module
|
||||||
Copyright by aceinnolab
|
Copyright by aceinnolab
|
||||||
"""
|
"""
|
||||||
|
import arrow # noqa
|
||||||
import arrow
|
|
||||||
|
|
||||||
from inkycal.custom import *
|
from inkycal.custom import *
|
||||||
from inkycal.modules.ical_parser import iCalendar
|
from inkycal.modules.ical_parser import iCalendar
|
||||||
from inkycal.modules.template import inkycal_module
|
from inkycal.modules.template import inkycal_module
|
||||||
@ -80,7 +78,7 @@ class Agenda(inkycal_module):
|
|||||||
self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize)
|
self.icon_font = ImageFont.truetype(fonts['MaterialIcons'], size=self.fontsize)
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -90,7 +88,7 @@ class Agenda(inkycal_module):
|
|||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
|
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
|
@ -84,7 +84,7 @@ class Calendar(inkycal_module):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def flatten(values):
|
def flatten(values):
|
||||||
@ -100,7 +100,7 @@ class Calendar(inkycal_module):
|
|||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
events_height = 0
|
events_height = 0
|
||||||
|
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -265,7 +265,7 @@ class Calendar(inkycal_module):
|
|||||||
# find out how many lines can fit at max in the event section
|
# find out how many lines can fit at max in the event section
|
||||||
line_spacing = 2
|
line_spacing = 2
|
||||||
text_bbox_height = self.font.getbbox("hg")
|
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)
|
max_event_lines = events_height // (line_height + line_spacing)
|
||||||
|
|
||||||
# generate list of coordinates for each line
|
# generate list of coordinates for each line
|
||||||
@ -322,7 +322,7 @@ class Calendar(inkycal_module):
|
|||||||
im_colour,
|
im_colour,
|
||||||
grid[days],
|
grid[days],
|
||||||
(icon_width, icon_height),
|
(icon_width, icon_height),
|
||||||
radius=6,
|
radius=6
|
||||||
)
|
)
|
||||||
|
|
||||||
# Filter upcoming events until 4 weeks in the future
|
# Filter upcoming events until 4 weeks in the future
|
||||||
|
@ -60,7 +60,7 @@ class Feeds(inkycal_module):
|
|||||||
self.shuffle_feeds = config["shuffle_feeds"]
|
self.shuffle_feeds = config["shuffle_feeds"]
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Validate module-specific parameters"""
|
"""Validate module-specific parameters"""
|
||||||
@ -75,7 +75,7 @@ class Feeds(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -83,8 +83,9 @@ class Feeds(inkycal_module):
|
|||||||
|
|
||||||
# Check if internet is available
|
# Check if internet is available
|
||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.debug('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
@ -23,16 +23,18 @@ from icons.weather_icons.weather_icons import get_weather_icon
|
|||||||
from inkycal.custom.functions import fonts
|
from inkycal.custom.functions import fonts
|
||||||
from inkycal.custom.functions import get_system_tz
|
from inkycal.custom.functions import get_system_tz
|
||||||
from inkycal.custom.functions import internet_available
|
from inkycal.custom.functions import internet_available
|
||||||
from inkycal.custom.functions import top_level
|
|
||||||
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
|
from inkycal.custom.inkycal_exceptions import NetworkNotReachableError
|
||||||
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
|
from inkycal.custom.openweathermap_wrapper import OpenWeatherMap
|
||||||
from inkycal.modules.inky_image import image_to_palette
|
from inkycal.modules.inky_image import image_to_palette
|
||||||
from inkycal.modules.template import inkycal_module
|
from inkycal.modules.template import inkycal_module
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
icons_dir = os.path.join(top_level, "icons", "ui-icons")
|
settings = Settings()
|
||||||
|
|
||||||
|
icons_dir = os.path.join(settings.FONT_PATH, "ui-icons")
|
||||||
|
|
||||||
|
|
||||||
def outline(image: Image, size: int, color: tuple) -> Image:
|
def outline(image: Image, size: int, color: tuple) -> Image:
|
||||||
@ -139,7 +141,7 @@ class Fullweather(inkycal_module):
|
|||||||
|
|
||||||
# Check if all required parameters are present
|
# Check if all required parameters are present
|
||||||
for param in self.requires:
|
for param in self.requires:
|
||||||
if not param in config:
|
if param not in config:
|
||||||
raise Exception(f"config is missing {param}")
|
raise Exception(f"config is missing {param}")
|
||||||
|
|
||||||
# required parameters
|
# required parameters
|
||||||
@ -237,7 +239,7 @@ class Fullweather(inkycal_module):
|
|||||||
self.left_section_width = int(self.width / 4)
|
self.left_section_width = int(self.width / 4)
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def createBaseImage(self):
|
def createBaseImage(self):
|
||||||
"""
|
"""
|
||||||
|
@ -50,7 +50,7 @@ class Inkyimage(inkycal_module):
|
|||||||
self.dither = False
|
self.dither = False
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -71,7 +71,7 @@ class Inkyimage(inkycal_module):
|
|||||||
# Remove background if present
|
# Remove background if present
|
||||||
im.remove_alpha()
|
im.remove_alpha()
|
||||||
|
|
||||||
# if autoflip was enabled, flip the image
|
# if auto-flip was enabled, flip the image
|
||||||
if self.autoflip:
|
if self.autoflip:
|
||||||
im.autoflip(self.orientation)
|
im.autoflip(self.orientation)
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class Jokes(inkycal_module):
|
|||||||
config = config['config']
|
config = config['config']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -39,7 +39,7 @@ class Jokes(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'image size: {im_width} x {im_height} px')
|
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -47,8 +47,9 @@ class Jokes(inkycal_module):
|
|||||||
|
|
||||||
# Check if internet is available
|
# Check if internet is available
|
||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.debug('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
@ -67,7 +67,7 @@ class Inkyserver(inkycal_module):
|
|||||||
self.path_body = config['path_body']
|
self.path_body = config['path_body']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
|
@ -8,13 +8,13 @@ from inkycal.custom import *
|
|||||||
# PIL has a class named Image, use alias for Inkyimage -> Images
|
# PIL has a class named Image, use alias for Inkyimage -> Images
|
||||||
from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette
|
from inkycal.modules.inky_image import Inkyimage as Images, image_to_palette
|
||||||
from inkycal.modules.template import inkycal_module
|
from inkycal.modules.template import inkycal_module
|
||||||
|
from inkycal.utils import JSONCache
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Slideshow(inkycal_module):
|
class Slideshow(inkycal_module):
|
||||||
"""Cycles through images in a local image folder
|
"""Cycles through images in a local image folder"""
|
||||||
"""
|
|
||||||
name = "Slideshow - cycle through images from a local folder"
|
name = "Slideshow - cycle through images from a local folder"
|
||||||
|
|
||||||
requires = {
|
requires = {
|
||||||
@ -53,7 +53,7 @@ class Slideshow(inkycal_module):
|
|||||||
|
|
||||||
# required parameters
|
# required parameters
|
||||||
for param in self.requires:
|
for param in self.requires:
|
||||||
if not param in config:
|
if param not in config:
|
||||||
raise Exception(f'config is missing {param}')
|
raise Exception(f'config is missing {param}')
|
||||||
|
|
||||||
# optional parameters
|
# optional parameters
|
||||||
@ -64,19 +64,20 @@ class Slideshow(inkycal_module):
|
|||||||
|
|
||||||
# Get the full path of all png/jpg/jpeg images in the given folder
|
# Get the full path of all png/jpg/jpeg images in the given folder
|
||||||
all_files = glob.glob(f'{self.path}/*')
|
all_files = glob.glob(f'{self.path}/*')
|
||||||
self.images = [i for i in all_files
|
self.images = [i for i in all_files if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
|
||||||
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
|
|
||||||
|
|
||||||
if not self.images:
|
if not self.images:
|
||||||
logger.error('No images found in the given folder, please '
|
logger.error('No images found in the given folder, please double check your path!')
|
||||||
'double check your path!')
|
|
||||||
raise Exception('No images found in the given folder path :/')
|
raise Exception('No images found in the given folder path :/')
|
||||||
|
|
||||||
|
self.cache = JSONCache('inkycal_slideshow')
|
||||||
|
self.cache_data = self.cache.read()
|
||||||
|
|
||||||
# set a 'first run' signal
|
# set a 'first run' signal
|
||||||
self._first_run = True
|
self._first_run = True
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -86,17 +87,19 @@ class Slideshow(inkycal_module):
|
|||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
|
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# rotates list items by 1 index
|
# rotates list items by 1 index
|
||||||
def rotate(somelist):
|
def rotate(list: list):
|
||||||
return somelist[1:] + somelist[:1]
|
return list[1:] + list[:1]
|
||||||
|
|
||||||
# Switch to the next image if this is not the first run
|
# Switch to the next image if this is not the first run
|
||||||
if self._first_run:
|
if self._first_run:
|
||||||
self._first_run = False
|
self._first_run = False
|
||||||
|
self.cache_data["current_index"] = 0
|
||||||
else:
|
else:
|
||||||
self.images = rotate(self.images)
|
self.images = rotate(self.images)
|
||||||
|
self.cache_data["current_index"] = (self.cache_data["current_index"] + 1) % len(self.images)
|
||||||
|
|
||||||
# initialize custom image class
|
# initialize custom image class
|
||||||
im = Images()
|
im = Images()
|
||||||
@ -110,7 +113,7 @@ class Slideshow(inkycal_module):
|
|||||||
# Remove background if present
|
# Remove background if present
|
||||||
im.remove_alpha()
|
im.remove_alpha()
|
||||||
|
|
||||||
# if autoflip was enabled, flip the image
|
# if auto-flip was enabled, flip the image
|
||||||
if self.autoflip:
|
if self.autoflip:
|
||||||
im.autoflip(self.orientation)
|
im.autoflip(self.orientation)
|
||||||
|
|
||||||
@ -123,6 +126,8 @@ class Slideshow(inkycal_module):
|
|||||||
# with the images now send, clear the current image
|
# with the images now send, clear the current image
|
||||||
im.clear()
|
im.clear()
|
||||||
|
|
||||||
|
self.cache.write(self.cache_data)
|
||||||
|
|
||||||
# return images
|
# return images
|
||||||
return im_black, im_colour
|
return im_black, im_colour
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class Stocks(inkycal_module):
|
|||||||
self.tickers = config['tickers']
|
self.tickers = config['tickers']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -63,7 +63,7 @@ class Stocks(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'image size: {im_width} x {im_height} px')
|
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels (required)
|
# Create an image for black pixels and one for coloured pixels (required)
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -142,7 +142,7 @@ class Stocks(inkycal_module):
|
|||||||
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
|
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
|
||||||
"default precision of 2 instead.")
|
"default precision of 2 instead.")
|
||||||
|
|
||||||
stockHistory = yfTicker.history("30d")
|
stockHistory = yfTicker.history("1mo")
|
||||||
stockHistoryLen = len(stockHistory)
|
stockHistoryLen = len(stockHistory)
|
||||||
logger.info(f'fetched {stockHistoryLen} datapoints ...')
|
logger.info(f'fetched {stockHistoryLen} datapoints ...')
|
||||||
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
|
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
|
||||||
|
@ -31,7 +31,7 @@ class TextToDisplay(inkycal_module):
|
|||||||
self.make_request = True if self.filepath.startswith("https://") else False
|
self.make_request = True if self.filepath.startswith("https://") else False
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Validate module-specific parameters"""
|
"""Validate module-specific parameters"""
|
||||||
@ -45,7 +45,7 @@ class TextToDisplay(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
|
@ -32,7 +32,7 @@ class Tindie(inkycal_module):
|
|||||||
# self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders
|
# self.mode = config['mode'] # unshipped_orders, shipped_orders, all_orders
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -40,7 +40,7 @@ class Tindie(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'image size: {im_width} x {im_height} px')
|
logger.debug(f'image size: {im_width} x {im_height} px')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -50,6 +50,7 @@ class Tindie(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
@ -56,7 +56,7 @@ class Todoist(inkycal_module):
|
|||||||
self._api = TodoistAPI(config['api_key'])
|
self._api = TodoistAPI(config['api_key'])
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'{__name__} loaded')
|
logger.debug(f'{__name__} loaded')
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
"""Validate module-specific parameters"""
|
"""Validate module-specific parameters"""
|
||||||
@ -70,7 +70,7 @@ class Todoist(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -80,6 +80,7 @@ class Todoist(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
# Set some parameters for formatting todos
|
# Set some parameters for formatting todos
|
||||||
|
@ -143,7 +143,7 @@ class Weather(inkycal_module):
|
|||||||
self.tempDispUnit = "°"
|
self.tempDispUnit = "°"
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f"{__name__} loaded")
|
logger.debug(f"{__name__} loaded")
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -152,7 +152,7 @@ class Weather(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info(f'Image size: {im_size}')
|
logger.debug(f'Image size: {im_size}')
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels
|
# Create an image for black pixels and one for coloured pixels
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -160,8 +160,9 @@ class Weather(inkycal_module):
|
|||||||
|
|
||||||
# Check if internet is available
|
# Check if internet is available
|
||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.debug('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise NetworkNotReachableError
|
raise NetworkNotReachableError
|
||||||
|
|
||||||
def get_moon_phase():
|
def get_moon_phase():
|
||||||
|
@ -83,7 +83,7 @@ class Webshot(inkycal_module):
|
|||||||
raise Exception("Rotation must be either 0, 90, 180 or 270")
|
raise Exception("Rotation must be either 0, 90, 180 or 270")
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'Inkycal webshot loaded')
|
logger.debug(f'Inkycal webshot loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
@ -99,7 +99,7 @@ class Webshot(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
logger.debug('image size: {} x {} px'.format(im_width, im_height))
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels (required)
|
# Create an image for black pixels and one for coloured pixels (required)
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -109,6 +109,7 @@ class Webshot(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise Exception('Network could not be reached :/')
|
raise Exception('Network could not be reached :/')
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -11,6 +11,8 @@ from inkycal.modules.template import inkycal_module
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
class Xkcd(inkycal_module):
|
class Xkcd(inkycal_module):
|
||||||
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
|
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
|
||||||
@ -51,13 +53,13 @@ class Xkcd(inkycal_module):
|
|||||||
self.scale_filter = config['filter']
|
self.scale_filter = config['filter']
|
||||||
|
|
||||||
# give an OK message
|
# give an OK message
|
||||||
print(f'Inkycal XKCD loaded')
|
logger.debug(f'Inkycal XKCD loaded')
|
||||||
|
|
||||||
def generate_image(self):
|
def generate_image(self):
|
||||||
"""Generate image for this module"""
|
"""Generate image for this module"""
|
||||||
|
|
||||||
# Create tmp path
|
# Create tmp path
|
||||||
tmpPath = f"{top_level}/temp"
|
tmpPath = settings.TEMPORARY_FOLDER
|
||||||
|
|
||||||
if not os.path.exists(tmpPath):
|
if not os.path.exists(tmpPath):
|
||||||
os.mkdir(tmpPath)
|
os.mkdir(tmpPath)
|
||||||
@ -66,7 +68,7 @@ class Xkcd(inkycal_module):
|
|||||||
im_width = int(self.width - (2 * self.padding_left))
|
im_width = int(self.width - (2 * self.padding_left))
|
||||||
im_height = int(self.height - (2 * self.padding_top))
|
im_height = int(self.height - (2 * self.padding_top))
|
||||||
im_size = im_width, im_height
|
im_size = im_width, im_height
|
||||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
logger.debug('image size: {} x {} px'.format(im_width, im_height))
|
||||||
|
|
||||||
# Create an image for black pixels and one for coloured pixels (required)
|
# Create an image for black pixels and one for coloured pixels (required)
|
||||||
im_black = Image.new('RGB', size=im_size, color='white')
|
im_black = Image.new('RGB', size=im_size, color='white')
|
||||||
@ -76,6 +78,7 @@ class Xkcd(inkycal_module):
|
|||||||
if internet_available():
|
if internet_available():
|
||||||
logger.info('Connection test passed')
|
logger.info('Connection test passed')
|
||||||
else:
|
else:
|
||||||
|
logger.error("Network not reachable. Please check your connection.")
|
||||||
raise Exception('Network could not be reached :/')
|
raise Exception('Network could not be reached :/')
|
||||||
|
|
||||||
# Set some parameters for formatting feeds
|
# Set some parameters for formatting feeds
|
||||||
|
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"
|
2
inkycal/utils/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .pisugar import PiSugar
|
||||||
|
from .json_cache import JSONCache
|
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)
|
147
inkycal/utils/pisugar.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""PiSugar helper class for Inkycal."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from inkycal.settings import Settings
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PiSugar:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# replace "command" with actual command
|
||||||
|
self.command_template = 'echo "command" | nc -q 0 127.0.0.1 8423'
|
||||||
|
self.allowed_commands = ["get battery", "get model", "get rtc_time", "get rtc_alarm_enabled",
|
||||||
|
"get rtc_alarm_time", "get alarm_repeat", "rtc_pi2rtc", "rtc_alarm_set"]
|
||||||
|
|
||||||
|
def _get_output(self, command, param=None):
|
||||||
|
if command not in self.allowed_commands:
|
||||||
|
logger.error(f"Command {command} not allowed")
|
||||||
|
return None
|
||||||
|
if param:
|
||||||
|
cmd = self.command_template.replace("command", f"{command} {param}")
|
||||||
|
else:
|
||||||
|
cmd = self.command_template.replace("command", command)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Command failed with {result.stderr}")
|
||||||
|
return None
|
||||||
|
output = result.stdout.strip()
|
||||||
|
return output
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing command: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_battery(self) -> float or None:
|
||||||
|
"""Get the battery level in percentage.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int or None: The battery level in percentage or None if the command fails.
|
||||||
|
"""
|
||||||
|
battery_output = self._get_output("get battery")
|
||||||
|
if battery_output:
|
||||||
|
for line in battery_output.splitlines():
|
||||||
|
if 'battery:' in line:
|
||||||
|
return float(line.split(':')[1].strip())
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_model(self) -> str or None:
|
||||||
|
"""Get the PiSugar model."""
|
||||||
|
model_output = self._get_output("get model")
|
||||||
|
if model_output:
|
||||||
|
for line in model_output.splitlines():
|
||||||
|
if 'model:' in line:
|
||||||
|
return line.split(':')[1].strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_rtc_time(self) -> arrow.arrow or None:
|
||||||
|
"""Get the RTC time."""
|
||||||
|
result = self._get_output("get rtc_time")
|
||||||
|
if result:
|
||||||
|
rtc_time = result.split("rtc_time: ")[1].strip()
|
||||||
|
return arrow.get(rtc_time)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_rtc_alarm_enabled(self) -> str or None:
|
||||||
|
"""Get the RTC alarm enabled status."""
|
||||||
|
result = self._get_output("get rtc_alarm_enabled")
|
||||||
|
if result:
|
||||||
|
second_line = result.splitlines()[1]
|
||||||
|
output = second_line.split('rtc_alarm_enabled: ')[1].strip()
|
||||||
|
return True if output == "true" else False
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_rtc_alarm_time(self) -> arrow.arrow or None:
|
||||||
|
"""Get the RTC alarm time."""
|
||||||
|
result = self._get_output("get rtc_alarm_time")
|
||||||
|
if result:
|
||||||
|
alarm_time = result.split('rtc_alarm_time: ')[1].strip()
|
||||||
|
return arrow.get(alarm_time)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_alarm_repeat(self) -> dict or None:
|
||||||
|
"""Get the alarm repeat status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict or None: A dictionary with the alarm repeating days or None if the command fails.
|
||||||
|
"""
|
||||||
|
result = self._get_output("get alarm_repeat")
|
||||||
|
if result:
|
||||||
|
repeating_days = f"{int(result.split('alarm_repeat: ')[1].strip()):8b}".strip()
|
||||||
|
data = {"Monday": False, "Tuesday": False, "Wednesday": False, "Thursday": False, "Friday": False,
|
||||||
|
"Saturday": False, "Sunday": False}
|
||||||
|
if repeating_days[0] == "1":
|
||||||
|
data["Monday"] = True
|
||||||
|
if repeating_days[1] == "1":
|
||||||
|
data["Tuesday"] = True
|
||||||
|
if repeating_days[2] == "1":
|
||||||
|
data["Wednesday"] = True
|
||||||
|
if repeating_days[3] == "1":
|
||||||
|
data["Thursday"] = True
|
||||||
|
if repeating_days[4] == "1":
|
||||||
|
data["Friday"] = True
|
||||||
|
if repeating_days[5] == "1":
|
||||||
|
data["Saturday"] = True
|
||||||
|
if repeating_days[6] == "1":
|
||||||
|
data["Sunday"] = True
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
|
||||||
|
def rtc_pi2rtc(self) -> bool:
|
||||||
|
"""Sync the Pi time to RTC.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the sync was successful, False otherwise.
|
||||||
|
"""
|
||||||
|
result = self._get_output("rtc_pi2rtc")
|
||||||
|
if result:
|
||||||
|
status = result.split('rtc_pi2rtc: ')[1].strip()
|
||||||
|
if status == "done":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def rtc_alarm_set(self, time: arrow.arrow, repeat:int=127) -> bool:
|
||||||
|
"""Set the RTC alarm time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time (arrow.arrow): The alarm time in ISO 8601 format.
|
||||||
|
repeat: int representing 7-bit binary number of repeating days. e.g. 127 = 1111111 = repeat every day
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the alarm was set successfully, False otherwise.
|
||||||
|
"""
|
||||||
|
iso_format = time.isoformat()
|
||||||
|
result = self._get_output("rtc_alarm_set", f"{iso_format } {repeat}")
|
||||||
|
if result:
|
||||||
|
status = result.split('rtc_alarm_set: ')[1].strip()
|
||||||
|
if status == "done":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
@ -51,4 +51,4 @@ virtualenv==20.25.0
|
|||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
x-wr-timezone==0.0.6
|
x-wr-timezone==0.0.6
|
||||||
xkcd==2.4.2
|
xkcd==2.4.2
|
||||||
yfinance==0.2.36
|
yfinance==0.2.40
|
||||||
|
@ -28,7 +28,7 @@ tests = [
|
|||||||
"padding_x": 10,
|
"padding_x": 10,
|
||||||
"padding_y": 10,
|
"padding_y": 10,
|
||||||
"fontsize": 12,
|
"fontsize": 12,
|
||||||
"language": "en"
|
"language": "de"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -6,10 +6,15 @@ import logging
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from inkycal.modules import Webshot
|
from inkycal.modules import Webshot
|
||||||
|
from inkycal.modules.inky_image import Inkyimage
|
||||||
|
from tests import Config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
preview = Inkyimage.preview
|
||||||
|
merge = Inkyimage.merge
|
||||||
|
|
||||||
tests = [
|
tests = [
|
||||||
{
|
{
|
||||||
"position": 1,
|
"position": 1,
|
||||||
@ -63,6 +68,7 @@ class TestWebshot(unittest.TestCase):
|
|||||||
for test in tests:
|
for test in tests:
|
||||||
logger.info(f'test {tests.index(test) + 1} generating image..')
|
logger.info(f'test {tests.index(test) + 1} generating image..')
|
||||||
module = Webshot(test)
|
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')
|
logger.info('OK')
|
||||||
|
|
||||||
|
@ -21,9 +21,9 @@ class TestMain(unittest.TestCase):
|
|||||||
assert inkycal.settings["info_section_height"] == 70
|
assert inkycal.settings["info_section_height"] == 70
|
||||||
assert inkycal.settings["border_around_modules"] is True
|
assert inkycal.settings["border_around_modules"] is True
|
||||||
|
|
||||||
def test_run(self):
|
def test_dry_run(self):
|
||||||
inkycal = Inkycal(self.settings_path, render=False)
|
inkycal = Inkycal(self.settings_path, render=False)
|
||||||
inkycal.test()
|
inkycal.dry_run()
|
||||||
|
|
||||||
def test_countdown(self):
|
def test_countdown(self):
|
||||||
inkycal = Inkycal(self.settings_path, render=False)
|
inkycal = Inkycal(self.settings_path, render=False)
|
||||||
|