Merge branch 'main' into weather_scaling

This commit is contained in:
mrbwburns
2023-11-26 17:16:03 +01:00
committed by GitHub
50 changed files with 1360 additions and 894 deletions

View File

@@ -10,7 +10,9 @@ import inkycal.modules.inkycal_todoist
import inkycal.modules.inkycal_image
import inkycal.modules.inkycal_jokes
import inkycal.modules.inkycal_slideshow
# import inkycal.modules.inkycal_server
import inkycal.modules.inkycal_stocks
import inkycal.modules.inkycal_webshot
import inkycal.modules.inkycal_xkcd
# Main file
from inkycal.main import Inkycal

View File

@@ -1,2 +1,3 @@
from .functions import *
from .inkycal_exceptions import *
from .openweathermap_wrapper import OpenWeatherMap

View File

@@ -1,15 +1,15 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Inkycal custom-functions for ease-of-use
Copyright by aceinnolab
"""
import logging
from PIL import Image, ImageDraw, ImageFont, ImageColor
from urllib.request import urlopen
import os
import time
import traceback
import requests
from PIL import ImageFont, ImageDraw, Image
logs = logging.getLogger(__name__)
logs.setLevel(level=logging.INFO)
@@ -98,11 +98,13 @@ def auto_fontsize(font, max_height):
Returns:
A PIL font object with modified height.
"""
fontsize = font.getsize('hg')[1]
while font.getsize('hg')[1] <= (max_height * 0.80):
text_bbox = font.getbbox("hg")
text_height = text_bbox[3]
fontsize = text_height
while text_height <= (max_height * 0.80):
fontsize += 1
font = ImageFont.truetype(font.path, fontsize)
text_height = text_bbox[3]
return font
@@ -154,21 +156,34 @@ def write(image, xy, box_size, text, font=None, **kwargs):
if autofit or (fill_width != 1.0) or (fill_height != 0.8):
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1]
while (text_width < int(box_width * fill_width) and
text_height < int(box_height * fill_height)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1]
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1]
# Truncate text if text is too long so it can fit inside the box
if (text_width, text_height) > (box_width, box_height):
logs.debug(('truncating {}'.format(text)))
while (text_width, text_height) > (box_width, box_height):
text = text[0:-1]
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
text_bbox = font.getbbox(text)
text_width = text_bbox[2] - text_bbox[0]
text_bbox_height = font.getbbox("hg")
text_height = text_bbox_height[3] - text_bbox_height[1]
logs.debug(text)
# Align text to desired position
@@ -215,14 +230,17 @@ def text_wrap(text, font=None, max_width=None):
A list containing chunked strings of the full text.
"""
lines = []
if font.getsize(text)[0] < max_width:
text_width = font.getlength(text)
if text_width < max_width:
lines.append(text)
else:
words = text.split(' ')
i = 0
while i < len(words):
line = ''
while i < len(words) and font.getsize(line + words[i])[0] <= max_width:
while i < len(words) and font.getlength(line + words[i]) <= max_width:
line = line + words[i] + " "
i += 1
if not line:
@@ -247,12 +265,14 @@ def internet_available():
>>> if internet_available():
>>> #...do something that requires internet connectivity
"""
try:
urlopen('https://google.com', timeout=5)
return True
except:
return False
for attempt in range(3):
try:
requests.get('https://google.com', timeout=5)
return True
except:
print(f"Network could not be reached: {traceback.print_exc()}")
time.sleep(5)
return False
def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
@@ -279,17 +299,17 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
colour = 'black'
# size from function paramter
# size from function parameter
width, height = int(size[0] * (1 - shrinkage[0])), int(size[1] * (1 - shrinkage[1]))
# shift cursor to move rectangle to center
offset_x, offset_y = int((size[0] - width) / 2), int((size[1] - height) / 2)
x, y, diameter = xy[0] + offset_x, xy[1] + offset_y, radius * 2
# lenght of rectangle size
# length of rectangle size
a, b = (width - diameter), (height - diameter)
# Set coordinates for staright lines
# Set coordinates for straight lines
p1, p2 = (x + radius, y), (x + radius + a, y)
p3, p4 = (x + width, y + radius), (x + width, y + radius + b)
p5, p6 = (p2[0], y + height), (p1[0], y + height)
@@ -313,3 +333,12 @@ def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
draw.arc((c3, c4), 270, 360, fill=colour, width=thickness)
draw.arc((c5, c6), 0, 90, fill=colour, width=thickness)
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)
def draw_border_2(im: Image, xy: tuple, size: tuple, radius: int):
draw = ImageDraw.Draw(im)
x, y = xy
w, h = size
draw.rounded_rectangle(xy=(x, y, x + w, y + h), outline="black", radius=radius)

View File

@@ -0,0 +1,43 @@
import logging
from enum import Enum
import requests
import json
logger = logging.getLogger(__name__)
class WEATHER_OPTIONS(Enum):
CURRENT_WEATHER = "weather"
class FORECAST_INTERVAL(Enum):
THREE_HOURS = "3h"
FIVE_DAYS = "5d"
class OpenWeatherMap:
def __init__(self, api_key:str, city_id:int, units:str) -> None:
self.api_key = api_key
self.city_id = city_id
assert (units in ["metric", "imperial"] )
self.units = units
self._api_version = "2.5"
self._base_url = f"https://api.openweathermap.org/data/{self._api_version}"
def get_current_weather(self) -> dict:
current_weather_url = f"{self._base_url}/weather?id={self.city_id}&appid={self.api_key}&units={self.units}"
response = requests.get(current_weather_url)
if not response.ok:
raise AssertionError(f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}")
data = json.loads(response.text)
return data
def get_weather_forecast(self) -> dict:
forecast_url = f"{self._base_url}/forecast?id={self.city_id}&appid={self.api_key}&units={self.units}"
response = requests.get(forecast_url)
if not response.ok:
raise AssertionError(f"Failure getting the current weather: code {response.status_code}. Reason: {response.text}")
data = json.loads(response.text)["list"]
return data

View File

@@ -4,6 +4,7 @@ Copyright by aceisace
"""
import os
import logging
import traceback
from importlib import import_module
from PIL import Image
@@ -43,7 +44,7 @@ class Display:
except FileNotFoundError:
raise Exception('SPI could not be found. Please check if SPI is enabled')
def render(self, im_black: Image.Image, im_colour=Image.Image or None) -> None:
def render(self, im_black: Image, im_colour=Image or None) -> None:
"""Renders an image on the selected E-Paper display.
Initlializes the E-Paper display, sends image data and executes command
@@ -66,7 +67,6 @@ class Display:
Rendering black-white on coloured E-Paper displays:
>>> sample_image = Image.open('path/to/file.png')
>>> display = Display('my_coloured_display')
>>> display.render(sample_image, sample_image)
@@ -82,14 +82,7 @@ class Display:
epaper = self._epaper
if not self.supports_colour:
print('Initialising..', end='')
epaper.init()
print('Updating display......', end='')
epaper.display(epaper.getbuffer(im_black))
print('Done')
elif self.supports_colour:
if self.supports_colour:
if not im_colour:
raise Exception('im_colour is required for coloured epaper displays')
print('Initialising..', end='')
@@ -97,6 +90,12 @@ class Display:
print('Updating display......', end='')
epaper.display(epaper.getbuffer(im_black), epaper.getbuffer(im_colour))
print('Done')
else:
print('Initialising..', end='')
epaper.init()
print('Updating display......', end='')
epaper.display(epaper.getbuffer(im_black))
print('Done')
print('Sending E-Paper to deep sleep...', end='')
epaper.sleep()
@@ -173,9 +172,10 @@ class Display:
try:
driver = import_driver(model_name)
return driver.EPD_WIDTH, driver.EPD_HEIGHT
except Exception as e:
except:
logging.error(f'Failed to load driver for ${model_name}. Check spelling?')
raise e;
print(traceback.format_exc())
raise AssertionError("Could not import driver")
@classmethod
def get_display_names(cls) -> list:

View File

@@ -35,6 +35,8 @@ from inkycal.display.drivers import epdconfig
EPD_WIDTH = 800
EPD_HEIGHT = 480
logger = logging.getLogger(__name__)
class EPD:
def __init__(self):
@@ -48,11 +50,11 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(20)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(2)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(20)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -66,14 +68,21 @@ class EPD:
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def send_data2(self, data):
epdconfig.digital_write(self.dc_pin, 1)
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.SPI.writebytes2(data)
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
logger.debug("e-Paper busy")
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
while (busy == 0):
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
epdconfig.delay_ms(200)
epdconfig.delay_ms(20)
logger.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
@@ -81,6 +90,12 @@ class EPD:
# EPD hardware init start
self.reset()
self.send_command(0x06) # btst
self.send_data(0x17)
self.send_data(0x17)
self.send_data(0x28) # If an exception is displayed, try using 0x38
self.send_data(0x17)
self.send_command(0x01) # POWER SETTING
self.send_data(0x07)
self.send_data(0x07) # VGH=20V,VGL=-20V
@@ -114,47 +129,39 @@ class EPD:
return 0
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
img = image
imwidth, imheight = img.size
if (imwidth == self.width and imheight == self.height):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
img = img.convert('1')
elif (imwidth == self.height and imheight == self.width):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
# image has correct dimensions, but needs to be rotated
img = img.rotate(90, expand=True).convert('1')
else:
logger.warning("Wrong image dimensions: must be " + str(self.width) + "x" + str(self.height))
# return a blank buffer
return [0x00] * (int(self.width / 8) * self.height)
buf = bytearray(img.tobytes('raw'))
# The bytes need to be inverted, because in the PIL world 0=black and 1=white, but
# in the e-paper world 0=white and 1=black.
for i in range(len(buf)):
buf[i] ^= 0xFF
return buf
def display(self, image):
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(~image[i]);
self.send_data2(image)
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
buf = [0x00] * (int(self.width / 8) * self.height)
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_data2(buf)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_data2(buf)
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
@@ -166,5 +173,6 @@ class EPD:
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.delay_ms(2000)
epdconfig.module_exit()
### END OF FILE ###
### END OF FILE ###

View File

@@ -1,6 +1,3 @@
#!python3
# -*- coding: utf-8 -*-
"""
Main class for inkycal Project
Copyright by aceinnolab
@@ -9,11 +6,12 @@ Copyright by aceinnolab
import glob
import hashlib
import json
import traceback
from logging.handlers import RotatingFileHandler
import arrow
import numpy
import asyncio
from inkycal.custom import *
from inkycal.display import Display
@@ -27,7 +25,6 @@ stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.ERROR)
if not os.path.exists(f'{top_level}/logs'):
os.mkdir(f'{top_level}/logs')
@@ -37,9 +34,7 @@ logging.basicConfig(
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
@@ -71,15 +66,18 @@ class Inkycal:
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
"""
def __init__(self, settings_path=None, render=True):
def __init__(self, settings_path:str or None=None, render:bool=True):
"""Initialise Inkycal"""
self._release = '2.0.2'
# Get the release version from setup.py
with open(f'{top_level}/setup.py') as setup_file:
for line in setup_file:
if line.startswith('__version__'):
self._release = line.split("=")[-1].replace("'", "").replace('"', "").replace(" ", "")
break
# Check if render was set correctly
if render not in [True, False]:
raise Exception(f'render must be True or False, not "{render}"')
self.render = render
self.info = None
# load settings file - throw an error if file could not be found
if settings_path:
@@ -89,7 +87,7 @@ class Inkycal:
self.settings = settings
except FileNotFoundError:
raise SettingsFileNotFoundError
raise FileNotFoundError(f"No settings.json file could be found in the specified location: {settings_path}")
else:
try:
@@ -106,6 +104,8 @@ class Inkycal:
# Option to use epaper image optimisation, reduces colours
self.optimize = True
self.show_border = self.settings.get('border_around_modules', False)
# Load drivers if image should be rendered
if self.render:
# Init Display class with model in settings file
@@ -121,7 +121,7 @@ class Inkycal:
# init calibration state
self._calibration_state = False
# Load and intialize modules specified in the settings file
# Load and initialise modules specified in the settings file
self._module_number = 1
for module in settings['modules']:
module_name = module['name']
@@ -168,10 +168,10 @@ class Inkycal:
update_timings = [(60 - int(interval_mins) * updates) for updates in
range(60 // int(interval_mins))][::-1]
# Calculate time in mins until next update
# Calculate time in minutes until next update
minutes = [_ for _ in update_timings if _ >= now.minute][0] - now.minute
# Print the remaining time in mins until next update
# Print the remaining time in minutes until next update
print(f'{minutes} minutes left until next refresh')
# Calculate time in seconds until next update
@@ -205,6 +205,8 @@ class Inkycal:
print(f'generating image(s) for {name}...', end="")
try:
black, colour = module.generate_image()
if self.show_border:
draw_border_2(im=black, xy=(1, 1), size=(black.width - 2, black.height - 2), radius=5)
black.save(f"{self.image_folder}module{number}_black.png", "PNG")
colour.save(f"{self.image_folder}module{number}_colour.png", "PNG")
print('OK!')
@@ -259,12 +261,12 @@ class Inkycal:
return res
def run(self):
async def run(self):
"""Runs main program in nonstop mode.
Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
from all modules, assembles them in one image, refreshed the E-Paper and
then sleeps until the next sheduled update.
then sleeps until the next scheduled update.
"""
# Get the time of initial run
@@ -300,6 +302,8 @@ class Inkycal:
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 "
@@ -327,7 +331,7 @@ class Inkycal:
self._calibration_check()
if self._calibration_state:
# after calibration we have to forcefully rewrite the screen
# after calibration, we have to forcefully rewrite the screen
self._remove_hashes(self.image_folder)
if self.supports_colour:
@@ -365,7 +369,7 @@ class Inkycal:
f'program started {runtime.humanize()}')
sleep_time = self.countdown()
time.sleep(sleep_time)
await asyncio.sleep(sleep_time)
@staticmethod
def _merge_bands():
@@ -536,7 +540,7 @@ class Inkycal:
self.Display.calibrate()
def _calibration_check(self):
"""Calibration sheduler
"""Calibration scheduler
uses calibration hours from settings file to check if calibration is due"""
now = arrow.now()
# print('hour:', now.hour, 'hours:', self._calibration_hours)
@@ -547,187 +551,6 @@ class Inkycal:
else:
self._calibration_state = False
@classmethod
def add_module(cls, filepath):
"""registers a third party module for inkycal.
Uses the full filepath of the third party module to check if it is inside
the correct folder, then checks if it's an inkycal module. Lastly, the
init files in /inkycal and /inkycal/modules are updated to allow using
the new module.
Args:
- filepath: The full filepath of the third party module. Modules should be
in Inkycal/inkycal/modules.
Usage:
- download a third-party module. The exact link is provided by the
developer of that module and starts with
`https://raw.githubusercontent.com/...`
enter the following in bash to download a module::
$ cd Inkycal/inkycal/modules #navigate to modules folder in inkycal
$ wget https://raw.githubusercontent.com/... #download the module
then register it with this function::
>>> from inkycal import Inkycal
>>> Inkycal.add_module('/full/path/to/the/module/in/inkycal/modules.py')
"""
module_folder = top_level + '/inkycal/modules'
if module_folder in filepath:
filename = filepath.split('.py')[0].split('/')[-1]
# Extract name of class from given module and validate if it's an inkycal
# module
with open(filepath, mode='r') as module:
module_content = module.read().splitlines()
for line in module_content:
if '(inkycal_module):' in line:
classname = line.split(' ')[-1].split('(')[0]
break
if not classname:
raise TypeError("your module doesn't seem to be a correct inkycal module.."
"Please check your module again.")
# Check if filename or classname exists in init of module folder
with open(module_folder + '/__init__.py', mode='r') as file:
module_init = file.read().splitlines()
print('checking module init file..')
for line in module_init:
if filename in line:
raise Exception(
"A module with this filename already exists! \n"
"Please consider renaming your module and try again."
)
if classname in line:
raise Exception(
"A module with this classname already exists! \n"
"Please consider renaming your class and try again."
)
print('OK!')
# Check if filename or classname exists in init of inkycal folder
with open(top_level + '/inkycal/__init__.py', mode='r') as file:
inkycal_init = file.read().splitlines()
print('checking inkycal init file..')
for line in inkycal_init:
if filename in line:
raise Exception(
"A module with this filename already exists! \n"
"Please consider renaming your module and try again."
)
if classname in line:
raise Exception(
"A module with this classname already exists! \n"
"Please consider renaming your class and try again."
)
print('OK')
# If all checks have passed, add the module in the module init file
with open(module_folder + '/__init__.py', mode='a') as file:
file.write(f'from .{filename} import {classname} # Added by module adder')
# If all checks have passed, add the module in the inkycal init file
with open(top_level + '/inkycal/__init__.py', mode='a') as file:
file.write(f'import inkycal.modules.{filename} # Added by module adder')
print(f"Your module '{filename}' with class '{classname}' has been added "
"successfully! Hooray!")
return
# Check if module is inside the modules folder
raise Exception(f"Your module should be in {module_folder} "
f"but is currently in {filepath}")
@classmethod
def remove_module(cls, filename, remove_file=True):
"""unregisters an inkycal module.
Looks for given filename.py in /modules folder, removes entries of that
module in init files inside /inkycal and /inkycal/modules
Args:
- filename: The filename (with .py ending) of the module which should be
unregistered. e.g. `'mymodule.py'`
- remove_file: ->bool (True/False). If set to True, the module is deleted
after unregistering it, else it remains in the /modules folder
Usage:
- Look for the module in Inkycal/inkycal/modules which should be removed.
Only the filename (with .py) is required, not the full path.
Use this function to unregister the module from inkycal::
>>> from inkycal import Inkycal
>>> Inkycal.remove_module('mymodule.py')
"""
module_folder = top_level + '/inkycal/modules'
# Check if module is inside the modules folder and extract classname
try:
with open(f"{module_folder}/{filename}", mode='r') as file:
module_content = file.read().splitlines()
for line in module_content:
if '(inkycal_module):' in line:
classname = line.split(' ')[-1].split('(')[0]
break
if not classname:
print('The module you are trying to remove is not an inkycal module.. '
'Not removing it.')
return
except FileNotFoundError:
print(f"No module named {filename} found in {module_folder}")
return
filename = filename.split('.py')[0]
# Create a memory backup of /modules init file
with open(module_folder + '/__init__.py', mode='r') as file:
module_init = file.read().splitlines()
print('removing line from module_init')
# Remove lines that contain classname
with open(module_folder + '/__init__.py', mode='w') as file:
for line in module_init:
if not classname in line:
file.write(line + '\n')
else:
print('found, removing')
# Create a memory backup of inkycal init file
with open(f"{top_level}/inkycal/__init__.py", mode='r') as file:
inkycal_init = file.read().splitlines()
print('removing line from inkycal init')
# Remove lines that contain classname
with open(f"{top_level}/inkycal/__init__.py", mode='w') as file:
for line in inkycal_init:
if filename in line:
print('found, removing')
else:
file.write(line + '\n')
# remove the file of the third party module if it exists and remove_file
# was set to True (default)
if os.path.exists(f"{module_folder}/{filename}.py") and remove_file is True:
print('deleting module file')
os.remove(f"{module_folder}/{filename}.py")
print(f"Your module '{filename}' with class '{classname}' was removed.")
if __name__ == '__main__':
print(f'running inkycal main in standalone/debug mode')

View File

@@ -8,3 +8,5 @@ from .inkycal_jokes import Jokes
from .inkycal_stocks import Stocks
from .inkycal_slideshow import Slideshow
from .inkycal_textfile_to_display import TextToDisplay
from .inkycal_webshot import Webshot
from .inkycal_xkcd import Xkcd

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal iCalendar parsing module
Copyright by aceinnolab
@@ -119,7 +117,7 @@ class iCalendar:
events = (
{
'title': events.get('SUMMARY').lstrip(),
'title': events.get('SUMMARY').lstrip() if events.get('SUMMARY') else "",
'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')

View File

@@ -83,12 +83,11 @@ class Inkyimage:
@staticmethod
def preview(image):
""""Previews an image on gpicview (only works on Rapsbian with Desktop).
"""
path = '/home/pi/Desktop/'
image.save(path + 'temp.png')
os.system("gpicview " + path + 'temp.png')
os.system('rm ' + path + 'temp.png')
"""Previews an image on gpicview (only works on Rapsbian with Desktop)."""
path = '~/temp'
image.save(path + '/temp.png')
os.system("gpicview " + path + '/temp.png')
os.system('rm ' + path + '/temp.png')
def _image_loaded(self):
"""returns True if image was loaded"""

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal Agenda Module
Copyright by aceinnolab
@@ -98,7 +96,9 @@ class Agenda(inkycal_module):
# Calculate the max number of lines that can fit on the image
line_spacing = 1
line_height = int(self.font.getsize('hg')[1]) + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = im_height // line_height
logger.debug(f'max lines: {max_lines}')
@@ -109,9 +109,11 @@ class Agenda(inkycal_module):
# Create a list of dates for the next days
agenda_events = [
{'begin': today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format, locale=self.language)}
{
'begin': today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format, locale=self.language)
}
for _ in range(max_lines)]
# Load icalendar from config
@@ -133,9 +135,9 @@ class Agenda(inkycal_module):
# parser.show_events()
# Set the width for date, time and event titles
date_width = int(max([self.font.getsize(
dates['begin'].format(self.date_format, locale=self.language))[0]
for dates in agenda_events]) * 1.2)
date_width = int(max([self.font.getlength(
dates['begin'].format(self.date_format, locale=self.language))
for dates in agenda_events]) * 1.2)
logger.debug(f'date_width: {date_width}')
# Calculate positions for each line
@@ -147,9 +149,10 @@ class Agenda(inkycal_module):
logger.info('Managed to parse events from urls')
# Find out how much space the event times take
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=self.language))[0]
for events in upcoming_events]) * 1.2)
time_width = int(max([self.font.getlength(
events['begin'].format(self.time_format, locale=self.language))
for events in upcoming_events]) * 1.2)
logger.debug(f'time_width: {time_width}')
# Calculate x-pos for time
@@ -224,7 +227,3 @@ class Agenda(inkycal_module):
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal Calendar Module
Copyright by aceinnolab
@@ -110,7 +108,8 @@ class Calendar(inkycal_module):
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
weekdays_height = int(self.font.getsize('hg')[1] * 1.25)
text_bbox_height = self.font.getbbox("hg")
weekdays_height = int((text_bbox_height[3] - text_bbox_height[1])* 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
@@ -182,15 +181,15 @@ class Calendar(inkycal_module):
]
logger.debug(f'weekday names: {weekday_names}')
for idx, weekday in enumerate(weekday_pos):
for index, weekday in enumerate(weekday_pos):
write(
im_black,
weekday,
(icon_width, weekdays_height),
weekday_names[idx],
weekday_names[index],
font=self.font,
autofit=True,
fill_height=1.0,
fill_height=0.9,
)
# Create a calendar template and flatten (remove nestings)
@@ -207,6 +206,10 @@ class Calendar(inkycal_module):
# remove zeros from calendar since they are not required
calendar_flat = [num for num in calendar_flat if num != 0]
# ensure all numbers have the same size
fontsize_numbers = int(min(icon_width, icon_height) * 0.5)
number_font = ImageFont.truetype(self.font.path, fontsize_numbers)
# Add the numbers on the correct positions
for number in calendar_flat:
if number != int(now.day):
@@ -215,9 +218,7 @@ class Calendar(inkycal_module):
grid[number],
(icon_width, icon_height),
str(number),
font=self.num_font,
fill_height=0.5,
fill_width=0.5,
font=number_font,
)
# Draw a red/black circle with the current day of month in white
@@ -262,10 +263,10 @@ class Calendar(inkycal_module):
from inkycal.modules.ical_parser import iCalendar
# find out how many lines can fit at max in the event section
line_spacing = 0
max_event_lines = events_height // (
self.font.getsize('hg')[1] + line_spacing
)
line_spacing = 2
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
max_event_lines = events_height // (line_height + line_spacing)
# generate list of coordinates for each line
events_offset = im_height - events_height
@@ -293,14 +294,27 @@ class Calendar(inkycal_module):
month_events = parser.get_events(month_start, month_end, self.timezone)
parser.sort()
self.month_events = month_events
# Initialize days_with_events as an empty list
days_with_events = []
# find out on which days of this month events are taking place
days_with_events = [
int(events['begin'].format('D')) for events in month_events
]
# Handle multi-day events by adding all days between start and end
for event in month_events:
start_date = event['begin'].date()
end_date = event['end'].date()
# Convert start and end dates to arrow objects with timezone
start = arrow.get(event['begin'].date(), tzinfo=self.timezone)
end = arrow.get(event['end'].date(), tzinfo=self.timezone)
# Use arrow's range function for generating dates
for day in arrow.Arrow.range('day', start, end):
day_num = int(day.format('D')) # get day number using arrow's format method
if day_num not in days_with_events:
days_with_events.append(day_num)
# remove duplicates (more than one event in a single day)
list(set(days_with_events)).sort()
days_with_events = sorted(set(days_with_events))
self._days_with_events = days_with_events
# Draw a border with specified parameters around days with events
@@ -329,31 +343,18 @@ class Calendar(inkycal_module):
# Find out how much space (width) the date format requires
lang = self.language
date_width = int(
max(
(
self.font.getsize(
events['begin'].format(self.date_format, locale=lang)
)[0]
for events in upcoming_events
)
)
* 1.1
date_width = int(max((
self.font.getlength(events['begin'].format(self.date_format, locale=lang))
for events in upcoming_events))* 1.1
)
time_width = int(
max(
(
self.font.getsize(
events['begin'].format(self.time_format, locale=lang)
)[0]
for events in upcoming_events
)
)
* 1.1
time_width = int(max((
self.font.getlength(events['begin'].format(self.time_format, locale=lang))
for events in upcoming_events))* 1.1
)
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
event_width_s = im_width - date_width - time_width
event_width_l = im_width - date_width
@@ -365,7 +366,13 @@ class Calendar(inkycal_module):
cursor = 0
for event in upcoming_events:
if cursor < len(event_lines):
the_name = event['title']
event_duration = (event['end'] - event['begin']).days
if event_duration > 1:
# Format the duration using Arrow's localization
days_translation = arrow.get().shift(days=event_duration).humanize(only_distance=True, locale=lang)
the_name = f"{event['title']} ({days_translation})"
else:
the_name = event['title']
the_date = event['begin'].format(self.date_format, locale=lang)
the_time = event['begin'].format(self.time_format, locale=lang)
# logger.debug(f"name:{the_name} date:{the_date} time:{the_time}")
@@ -411,19 +418,16 @@ class Calendar(inkycal_module):
cursor += 1
else:
symbol = '- '
while self.font.getsize(symbol)[0] < im_width * 0.9:
while self.font.getlength(symbol) < im_width * 0.9:
symbol += ' -'
write(
im_black,
event_lines[0],
(im_width, self.font.getsize(symbol)[1]),
(im_width, line_height),
symbol,
font=self.font,
)
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
Feeds module for InkyCal Project
Copyright by aceinnolab
@@ -91,9 +89,11 @@ class Feeds(inkycal_module):
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
max_lines = (im_height // (line_height + line_spacing))
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@@ -149,7 +149,3 @@ class Feeds(inkycal_module):
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
iCanHazDadJoke module for InkyCal Project
Special thanks to Erik Fredericks (@efredericks) for the template!
@@ -54,10 +52,11 @@ class Jokes(inkycal_module):
raise NetworkNotReachableError
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_spacing = 5
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = (im_height // (line_height + line_spacing))
logger.debug(f"max_lines: {max_lines}")
@@ -97,7 +96,3 @@ class Jokes(inkycal_module):
# Return images for black and colour channels
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -1,4 +1,3 @@
#!python3
"""
Stocks Module for Inkycal Project
@@ -10,26 +9,18 @@ Version 0.1: Migration to Inkycal 2.0.0b
by https://github.com/worstface
"""
import os
import logging
from inkycal.modules.template import inkycal_module
from inkycal.custom import write, internet_available
import os
from PIL import Image
from matplotlib import pyplot
try:
import yfinance as yf
except ImportError:
print('yfinance is not installed! Please install with:')
print('pip3 install yfinance')
from inkycal.custom import write, internet_available
from inkycal.modules.template import inkycal_module
try:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
except ImportError:
print('matplotlib is not installed! Please install with:')
print('pip3 install matplotlib')
import yfinance as yf
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
logger = logging.getLogger(__name__)
@@ -79,26 +70,24 @@ class Stocks(inkycal_module):
im_colour = Image.new('RGB', size=im_size, color='white')
# Create tmp path
tmpPath = '/tmp/inkycal_stocks/'
tmpPath = 'temp/'
try:
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
print(f"Successfully created tmp directory {tmpPath} ")
except OSError:
print(f"Creation of tmp directory {tmpPath} failed")
if not os.path.exists(tmpPath):
print(f"Creating tmp directory {tmpPath}")
os.mkdir(tmpPath)
# Check if internet is available
if internet_available() == True:
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = (im_height // (line_height + line_spacing))
logger.debug(f"max_lines: {max_lines}")
@@ -211,7 +200,7 @@ class Stocks(inkycal_module):
else:
parsed_tickers_colour.append("")
if (_ < len(tickerCount)):
if _ < len(tickerCount):
parsed_tickers.append("")
parsed_tickers_colour.append("")
@@ -232,9 +221,10 @@ class Stocks(inkycal_module):
logger.info(f'chartSpace is...{im_width} {im_height}')
logger.info(f'open chart ...{chartPath}')
chartImage = Image.open(chartPath)
chartImage.thumbnail((im_width / 4, line_height * 4), Image.BICUBIC)
chartImage.thumbnail((int(im_width / 4), int(line_height * 4)), Image.BICUBIC)
pyplot.close()
chartPasteX = im_width - (chartImage.width)
chartPasteX = im_width - chartImage.width
chartPasteY = line_height * 5 * _
logger.info(f'pasting chart image with index {_} to...{chartPasteX} {chartPasteY}')
@@ -265,6 +255,3 @@ class Stocks(inkycal_module):
# Save image of black and colour channel in image-folder
return im_black, im_colour
if __name__ == '__main__':
print('running module in standalone/debug mode')

View File

@@ -1,4 +1,3 @@
#!python3
"""
Textfile module for InkyCal Project
@@ -7,44 +6,29 @@ If the content is too long, it will be truncated from the back until it fits
Copyright by aceinnolab
"""
from inkycal.modules.template import inkycal_module
from inkycal.custom import *
from urllib.request import urlopen
from inkycal.custom import *
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class TextToDisplay(inkycal_module):
"""TextToDisplay module
"""TextToDisplay module - Display text from a local file on the display
"""
name = "Text module - Display text from a local file on the display"
requires = {
"filepath": {
"label": "Please enter a filepath or URL pointing to a .txt file",
},
}
def __init__(self, config):
"""Initialize inkycal_textfile_to_display module"""
super().__init__(config)
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if param not in config:
raise Exception(f'config is missing {param}')
# required parameters
self.filepath = config["filepath"]
self.make_request = True if self.filepath.startswith("https://") else False
# give an OK message
print(f'{__name__} loaded')
@@ -66,17 +50,12 @@ class TextToDisplay(inkycal_module):
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise NetworkNotReachableError
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_spacing = 4
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = im_height // line_height
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@@ -87,6 +66,11 @@ class TextToDisplay(inkycal_module):
if self.make_request:
logger.info("Detected http path, making request")
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise NetworkNotReachableError
file_content = urlopen(self.filepath).read().decode('utf-8')
else:
# Create list containing all lines
@@ -111,7 +95,3 @@ class TextToDisplay(inkycal_module):
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -1,5 +1,3 @@
#!python3
"""
Inkycal Todoist Module
Copyright by aceinnolab
@@ -86,9 +84,10 @@ class Todoist(inkycal_module):
# Set some parameters for formatting todos
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
text_bbox_height = self.font.getbbox("hg")
line_height = text_bbox_height[3] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
max_lines = im_height // line_height
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
@@ -197,7 +196,3 @@ class Todoist(inkycal_module):
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {__name__} in standalone/debug mode')

View File

@@ -12,7 +12,7 @@ import math
import decimal
import arrow
from pyowm.owm import OWM
from inkycal.custom import OpenWeatherMap
logger = logging.getLogger(__name__)
@@ -95,7 +95,7 @@ class Weather(inkycal_module):
self.use_beaufort = config['use_beaufort']
# additional configuration
self.owm = OWM(self.api_key).weather_manager()
self.owm = OpenWeatherMap(api_key=self.api_key, city_id=self.location, units=config['units'])
self.timezone = get_system_tz()
self.locale = config['language']
self.weatherfont = ImageFont.truetype(
@@ -104,6 +104,42 @@ class Weather(inkycal_module):
# give an OK message
print(f"{__name__} loaded")
@staticmethod
def mps_to_beaufort(meters_per_second:float) -> int:
"""Map meters per second to the beaufort scale.
Args:
meters_per_second:
float representing meters per seconds
Returns:
an integer of the beaufort scale mapping the input
"""
thresholds = [0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.7, 24.5, 28.4]
return next((i for i, threshold in enumerate(thresholds) if meters_per_second < threshold), 11)
@staticmethod
def mps_to_mph(meters_per_second:float) -> float:
"""Map meters per second to miles per hour, rounded to one decimal place.
Args:
meters_per_second:
float representing meters per seconds.
Returns:
float representing the input value in miles per hour.
"""
# 1 m/s is approximately equal to 2.23694 mph
miles_per_hour = meters_per_second * 2.23694
return round(miles_per_hour, 1)
@staticmethod
def celsius_to_fahrenheit(celsius:int or float):
"""Converts the given temperate from degrees Celsius to Fahrenheit."""
fahrenheit = (celsius * 9 / 5) + 32
return fahrenheit
def generate_image(self):
"""Generate image for this module"""
@@ -124,7 +160,11 @@ class Weather(inkycal_module):
raise NetworkNotReachableError
def get_moon_phase():
"""Calculate the current (approximate) moon phase"""
"""Calculate the current (approximate) moon phase
Returns:
The corresponding moonphase-icon.
"""
dec = decimal.Decimal
diff = now - arrow.get(2001, 1, 1)
@@ -154,7 +194,7 @@ class Weather(inkycal_module):
return answer
# Lookup-table for weather icons and weather codes
weathericons = {
weather_icons = {
'01d': '\uf00d',
'02d': '\uf002',
'03d': '\uf013',
@@ -227,26 +267,26 @@ class Weather(inkycal_module):
# Increase fontsize to fit specified height and width of text box
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
while (text_width < int(box_width * 0.9) and
text_height < int(box_height * 0.9)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
text_width, text_height = font.getsize(text)
text_width, text_height = font.getbbox(text)[2:]
# Align text to desired position
x = int((box_width / 2) - (text_width / 2))
y = int((box_height / 2) - (text_height / 2) - (icon_size_correction[icon] * size) / 2)
y = int((box_height / 2) - (text_height / 2))
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
if rotation != None:
if rotation:
space.rotate(rotation, expand=True)
# Update only region with text (add text with transparent background)
@@ -350,14 +390,9 @@ class Weather(inkycal_module):
temp_fc4 = (col7, row3)
# Create current-weather and weather-forecast objects
if self.location.isdigit():
logging.debug('looking up location by ID')
weather = self.owm.weather_at_id(int(self.location)).weather
forecast = self.owm.forecast_at_id(int(self.location), '3h')
else:
logging.debug('looking up location by string')
weather = self.owm.weather_at_place(self.location).weather
forecast = self.owm.forecast_at_place(self.location, '3h')
logging.debug('looking up location by ID')
weather = self.owm.get_current_weather()
forecast = self.owm.get_weather_forecast()
# Set decimals
dec_temp = None if self.round_temperature == True else 1
@@ -369,12 +404,14 @@ class Weather(inkycal_module):
elif self.units == 'imperial':
temp_unit = 'fahrenheit'
logging.debug(f'temperature unit: {temp_unit}')
logging.debug(f'temperature unit: {self.units}')
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
# Get current time
now = arrow.utcnow()
fc_data = {}
if self.forecast_interval == 'hourly':
logger.debug("getting hourly forecasts")
@@ -386,21 +423,22 @@ class Weather(inkycal_module):
else:
hour_gap = 3
# Create timings for hourly forcasts
# Create timings for hourly forecasts
forecast_timings = [now.shift(hours=+ hour_gap + _).floor('hour')
for _ in range(0, 12, 3)]
# Create forecast objects for given timings
forecasts = [forecast.get_weather_at(forecast_time.datetime) for
forecast_time in forecast_timings]
forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in forecast_timings]
# Add forecast-data to fc_data dictionary
fc_data = {}
for forecast in forecasts:
temp = '{}°'.format(round(
forecast.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if self.units == "metric":
temp = f"{round(weather['main']['temp'], ndigits=dec_temp)}°C"
else:
temp = f"{round(self.celsius_to_fahrenheit(weather['weather']['main']['temp']), ndigits=dec_temp)}°F"
icon = forecast.weather_icon_name
icon = forecast["weather"][0]["icon"]
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': temp,
'icon': icon,
@@ -412,38 +450,35 @@ class Weather(inkycal_module):
logger.debug("getting daily forecasts")
def calculate_forecast(days_from_today):
def calculate_forecast(days_from_today) -> dict:
"""Get temperature range and most frequent icon code for forecast
days_from_today should be int from 1-4: e.g. 2 -> 2 days from today
"""
# Create a list containing time-objects for every 3rd hour of the day
time_range = list(arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),
now.shift(days=days_from_today).ceil('day')
))[::3]
time_range = list(
arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),now.shift(days=days_from_today).ceil('day')
))[::3]
# Get forecasts for each time-object
forecasts = [forecast.get_weather_at(_.datetime) for _ in time_range]
forecasts = [_ for _ in forecast if arrow.get(_["dt"]) in time_range]
# Get all temperatures for this day
daily_temp = [round(_.temperature(unit=temp_unit)['temp'],
ndigits=dec_temp) for _ in forecasts]
daily_temp = [round(_["main"]["temp"]) for _ in forecasts]
# Calculate min. and max. temp for this day
temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°'
temp_range = f'{min(daily_temp)}°/{max(daily_temp)}°'
# Get all weather icon codes for this day
daily_icons = [_.weather_icon_name for _ in forecasts]
daily_icons = [_["weather"][0]["icon"] for _ in forecasts]
# Find most common element from all weather icon codes
status = max(set(daily_icons), key=daily_icons.count)
weekday = now.shift(days=days_from_today).format('ddd', locale=
self.locale)
weekday = now.shift(days=days_from_today).format('ddd', locale=self.locale)
return {'temp': temp_range, 'icon': status, 'stamp': weekday}
forecasts = [calculate_forecast(days) for days in range(1, 5)]
fc_data = {}
for forecast in forecasts:
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': forecast['temp'],
@@ -455,13 +490,15 @@ class Weather(inkycal_module):
logger.debug((key, val))
# Get some current weather details
temperature = '{}°'.format(round(
weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if dec_temp != 0:
temperature = f"{round(weather['main']['temp'])}°"
else:
temperature = f"{round(weather['main']['temp'],ndigits=dec_temp)}°"
weather_icon = weather.weather_icon_name
humidity = str(weather.humidity)
sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone)
sunset_raw = arrow.get(weather.sunset_time()).to(self.timezone)
weather_icon = weather["weather"][0]["icon"]
humidity = str(weather["main"]["humidity"])
sunrise_raw = arrow.get(weather["sys"]["sunrise"]).to(self.timezone)
sunset_raw = arrow.get(weather["sys"]["sunset"]).to(self.timezone)
logger.debug(f'weather_icon: {weather_icon}')
@@ -469,33 +506,29 @@ class Weather(inkycal_module):
logger.debug('using 12 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('h:mm a')
sunset = sunset_raw.format('h:mm a')
elif self.hour_format == 24:
else:
# 24 hours format
logger.debug('using 24 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('H:mm')
sunset = sunset_raw.format('H:mm')
# Format the windspeed to user preference
# Format the wind-speed to user preference
if self.use_beaufort:
logger.debug("using beaufort for wind")
wind = str(weather.wind(unit='beaufort')['speed'])
wind = str(self.mps_to_beaufort(weather["wind"]["speed"]))
else:
if self.units == 'metric':
logging.debug('getting windspeed in metric unit')
wind = str(weather.wind(unit='meters_sec')['speed']) + 'm/s'
logging.debug('getting wind speed in meters per second')
wind = f"{weather['wind']['speed']} m/s"
else:
logging.debug('getting wind speed in imperial unit')
wind = f"{self.mps_to_mph(weather['wind']['speed'])} miles/h"
elif self.units == 'imperial':
logging.debug('getting windspeed in imperial unit')
wind = str(weather.wind(unit='miles_hour')['speed']) + 'miles/h'
dec = decimal.Decimal
moonphase = get_moon_phase()
moon_phase = get_moon_phase()
# Fill weather details in col 1 (current weather icon)
draw_icon(im_colour, weather_icon_pos, (col_width, im_height),
weathericons[weather_icon])
weather_icons[weather_icon])
# Fill weather details in col 2 (temp, humidity, wind)
draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height),
@@ -521,7 +554,7 @@ class Weather(inkycal_module):
wind, font=self.font)
# Fill weather details in col 3 (moonphase, sunrise, sunset)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moonphase)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moon_phase)
draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051')
write(im_black, sunrise_time_pos, (col_width - icon_small, row_height),
@@ -535,7 +568,7 @@ class Weather(inkycal_module):
for pos in range(1, len(fc_data) + 1):
stamp = fc_data[f'fc{pos}']['stamp']
icon = weathericons[fc_data[f'fc{pos}']['icon']]
icon = weather_icons[fc_data[f'fc{pos}']['icon']]
temp = fc_data[f'fc{pos}']['temp']
write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height),
@@ -548,7 +581,7 @@ class Weather(inkycal_module):
border_h = row3 + row_height
border_w = col_width - 3 # leave 3 pixels gap
# Add borders around each sub-section
# Add borders around each subsection
draw_border(im_black, (col1, row1), (col_width * 3 - 3, border_h),
shrinkage=(0, 0))

View File

@@ -0,0 +1,164 @@
"""
Webshot module for Inkycal
by https://github.com/worstface
"""
from htmlwebshot import WebShot
from inkycal.custom import *
from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.modules.template import inkycal_module
from tests import Config
logger = logging.getLogger(__name__)
class Webshot(inkycal_module):
name = "Webshot - Displays screenshots of webpages"
# required parameters
requires = {
"url": {
"label": "Please enter the url",
},
"palette": {
"label": "Which color palette should be used for the webshots?",
"options": ["bw", "bwr", "bwy"]
}
}
optional = {
"crop_x": {
"label": "Please enter the crop x-position",
},
"crop_y": {
"label": "Please enter the crop y-position",
},
"crop_w": {
"label": "Please enter the crop width",
},
"crop_h": {
"label": "Please enter the crop height",
}
}
def __init__(self, config):
super().__init__(config)
config = config['config']
self.url = config['url']
self.palette = config['palette']
if "crop_h" in config and isinstance(config["crop_h"], str):
self.crop_h = int(config["crop_h"])
else:
self.crop_h = 2000
if "crop_w" in config and isinstance(config["crop_w"], str):
self.crop_w = int(config["crop_w"])
else:
self.crop_w = 2000
if "crop_x" in config and isinstance(config["crop_x"], str):
self.crop_x = int(config["crop_x"])
else:
self.crop_x = 0
if "crop_y" in config and isinstance(config["crop_y"], str):
self.crop_y = int(config["crop_y"])
else:
self.crop_y = 0
# give an OK message
print(f'Inkycal webshot loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpFolder = Config.TEMP_PATH
if not os.path.exists(tmpFolder):
print(f"Creating tmp directory {tmpFolder}")
os.mkdir(tmpFolder)
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info('image size: {} x {} px'.format(im_width, im_height))
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
logger.info(
f'preparing webshot from {self.url}... cropH{self.crop_h} cropW{self.crop_w} cropX{self.crop_x} cropY{self.crop_y}')
shot = WebShot()
shot.params = {
"--crop-x": self.crop_x,
"--crop-y": self.crop_y,
"--crop-w": self.crop_w,
"--crop-h": self.crop_h,
}
logger.info(f'getting webshot from {self.url}...')
try:
shot.create_pic(url=self.url, output=f"{tmpFolder}/webshot.png")
except:
print(traceback.format_exc())
print("If you have not already installed wkhtmltopdf, please use: sudo apt-get install wkhtmltopdf. See here for more details: https://github.com/1Danish-00/htmlwebshot/")
raise Exception('Could not get webshot :/')
logger.info(f'got webshot...')
webshotSpaceBlack = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
webshotSpaceColour = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
im = Images()
im.load(f'{tmpFolder}/webshot.png')
im.remove_alpha()
imageAspectRatio = im_width / im_height
webshotAspectRatio = im.image.width / im.image.height
if webshotAspectRatio > imageAspectRatio:
imageScale = im_width / im.image.width
else:
imageScale = im_height / im.image.height
webshotHeight = int(im.image.height * imageScale)
im.resize(width=int(im.image.width * imageScale), height=webshotHeight)
im_webshot_black, im_webshot_colour = im.to_palette(self.palette)
webshotCenterPosY = int((im_height / 2) - (im.image.height / 2))
centerPosX = int((im_width / 2) - (im.image.width / 2))
webshotSpaceBlack.paste(im_webshot_black, (centerPosX, webshotCenterPosY))
im_black.paste(webshotSpaceBlack)
webshotSpaceColour.paste(im_webshot_colour, (centerPosX, webshotCenterPosY))
im_colour.paste(webshotSpaceColour)
im.clear()
logger.info(f'added webshot image')
# Save image of black and colour channel in image-folder
return im_black, im_colour

View File

@@ -0,0 +1,202 @@
"""
Inkycal XKCD module
by https://github.com/worstface
"""
import xkcd
from inkycal.custom import *
from inkycal.modules.inky_image import Inkyimage as Images
from inkycal.modules.template import inkycal_module
logger = logging.getLogger(__name__)
class Xkcd(inkycal_module):
name = "xkcd - Displays comics from xkcd.com by Randall Munroe"
# required parameters
requires = {
"mode": {
"label": "Please select the mode",
"options": ["latest", "random"],
"default": "latest"
},
"palette": {
"label": "Which color palette should be used for the comic images?",
"options": ["bw", "bwr", "bwy"]
},
"alt": {
"label": "Would you like to add the alt text below the comic? If XKCD is not the only module you are showing, I recommend setting this to 'no'",
"options": ["yes", "no"],
"default": "no"
},
"filter": {
"label": "Would you like to add a scaling filter? If the is far too big to be shown in the space you've allotted for it, the module will try to find another image for you. This only applies in random mode. If XKCD is not the only module you are showing, I recommend setting this to 'no'.",
"options": ["yes", "no"],
"default": "no"
}
}
def __init__(self, config):
super().__init__(config)
config = config['config']
self.mode = config['mode']
self.palette = config['palette']
self.alt = config['alt']
self.scale_filter = config['filter']
# give an OK message
print(f'Inkycal XKCD loaded')
def generate_image(self):
"""Generate image for this module"""
# Create tmp path
tmpPath = f"{top_level}/temp"
if not os.path.exists(tmpPath):
os.mkdir(tmpPath)
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info('image size: {} x {} px'.format(im_width, im_height))
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Check if internet is available
if internet_available():
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# Set some parameters for formatting feeds
line_spacing = 1
text_bbox = self.font.getbbox("hg")
line_height = text_bbox[3] + line_spacing
line_width = im_width
max_lines = im_height // (line_height + line_spacing)
logger.debug(f"max_lines: {max_lines}")
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
# Calculate line_positions
line_positions = [(0, spacing_top + _ * line_height) for _ in range(max_lines)]
logger.debug(f'line positions: {line_positions}')
logger.info(f'getting xkcd comic...')
if self.mode == 'random':
if self.scale_filter == 'no':
xkcdComic = xkcd.getRandomComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
else:
perc = (2.1, 0.4)
url = "test variable, not a real comic"
while max(perc) > 1.75:
print("looking for another comic, old comic was: ", perc, url)
xkcdComic = xkcd.getRandomComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
actual_size = Image.open(tmpPath + '/xkcdComic.png').size
perc = (actual_size[0] / im_width, actual_size[1] / im_height)
url = xkcdComic.getImageLink()
print("found one! perc: ", perc, url)
else:
xkcdComic = xkcd.getLatestComic()
xkcdComic.download(output=tmpPath, outputFile='xkcdComic.png')
logger.info(f'got xkcd comic...')
title_lines = []
title_lines.append(xkcdComic.getTitle())
altOffset = int(line_height * 1)
if self.alt == "yes":
alt_text = xkcdComic.getAltText() # get the alt text, too (I break it up into multiple lines later on)
# break up the alt text into lines
alt_lines = []
current_line = ""
for _ in alt_text.split(" "):
# this breaks up the alt_text into words and creates each line by adding
# one word at a time until the line is longer than the width of the module
# then it appends the line to the alt_lines array and starts testing a new line
# with the next word
text_bbox = self.font.getbbox(current_line + _ + " ")
if text_bbox[2] < im_width:
current_line = current_line + _ + " "
else:
alt_lines.append(current_line)
current_line = _ + " "
alt_lines.append(
current_line) # this adds the last line to the array (or the only line, if the alt text is really short)
altHeight = int(line_height * len(alt_lines)) + altOffset
else:
altHeight = 0 # this is added so that I don't need to add more "if alt is yes" conditionals when centering below. Now the centering code will work regardless of whether they want alttext or not
comicSpaceBlack = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
comicSpaceColour = Image.new('RGBA', (im_width, im_height), (255, 255, 255, 255))
im = Images()
im.load(f"{tmpPath}/xkcdComic.png")
im.remove_alpha()
imageAspectRatio = im_width / im_height
comicAspectRatio = im.image.width / im.image.height
if comicAspectRatio > imageAspectRatio:
imageScale = im_width / im.image.width
else:
imageScale = im_height / im.image.height
comicHeight = int(im.image.height * imageScale)
headerHeight = int(line_height * 3 / 2)
if comicHeight + (headerHeight + altHeight) > im_height:
comicHeight -= (headerHeight + altHeight)
im.resize(width=int(im.image.width * imageScale), height=comicHeight)
im_comic_black, im_comic_colour = im.to_palette(self.palette)
headerCenterPosY = int((im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2))
comicCenterPosY = int((im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2) + headerHeight)
altCenterPosY = int(
(im_height / 2) - ((im.image.height + headerHeight + altHeight) / 2) + headerHeight + im.image.height)
centerPosX = int((im_width / 2) - (im.image.width / 2))
comicSpaceBlack.paste(im_comic_black, (centerPosX, comicCenterPosY))
im_black.paste(comicSpaceBlack)
comicSpaceColour.paste(im_comic_colour, (centerPosX, comicCenterPosY))
im_colour.paste(comicSpaceColour)
im.clear()
logger.info(f'added comic image')
# Write the title on the black image
write(im_black, (0, headerCenterPosY), (line_width, line_height),
title_lines[0], font=self.font, alignment='center')
if self.alt == "yes":
# write alt_text
for _ in range(len(alt_lines)):
write(im_black, (0, altCenterPosY + _ * line_height + altOffset), (line_width, line_height),
alt_lines[_], font=self.font, alignment='left')
# Save image of black and colour channel in image-folder
return im_black, im_colour

View File

@@ -1 +0,0 @@
from config import Config

View File

@@ -1,34 +0,0 @@
#!python
"""
Tests config
"""
import os
from dotenv import load_dotenv
# relative import
basedir = os.path.abspath(os.path.dirname(__file__))
# load config from corresponding file
load_dotenv(os.path.join(basedir, '.env'))
class Config:
get = os.environ.get
# show generated images via preview?
USE_PREVIEW = False
# ical_parser_test
OPENWEATHERMAP_API_KEY = get("OPENWEATHERMAP_API_KEY")
TEST_ICAL_URL = get("TEST_ICAL_URL")
# inkycal_agenda_test & inkycal_calendar_test
SAMPLE_ICAL_URL = get("SAMPLE_ICAL_URL")
# inkycal_todoist_test
TODOIST_API_KEY = get("TODOIST_API_KEY")

View File

@@ -1,56 +0,0 @@
#!python3
"""
iCalendar parser test (ical_parser)
"""
import logging
import os
import sys
import unittest
from urllib.request import urlopen
import arrow
from inkycal.modules.ical_parser import iCalendar
from inkycal.tests import Config
ical = iCalendar()
test_ical = Config.TEST_ICAL_URL
class ical_parser_test(unittest.TestCase):
def test_load_url(self):
print('testing loading via URL...', end="")
ical.load_url(test_ical)
print('OK')
def test_get_events(self):
print('testing parsing of events...', end="")
ical.get_events(arrow.now(), arrow.now().shift(weeks=30))
print('OK')
def test_sorting(self):
print('testing sorting of events...', end="")
ical.sort()
print('OK')
def test_show_events(self):
print('testing if events can be shown...', end="")
ical.show_events()
print('OK')
def test_laod_from_file(self):
print('testing loading from file...', end="")
dummy = str(urlopen(test_ical, timeout=10).read().decode())
with open('dummy.ical', mode="w", encoding="utf-8") as file:
file.write(dummy)
ical.load_from_file('dummy.ical')
print('OK')
os.remove('dummy.ical')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,85 +0,0 @@
#!python3
"""
inkycal_agenda unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Agenda as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
sample_url = Config.SAMPLE_ICAL_URL
tests = [
{
"name": "Agenda",
"config": {
"size": [400, 200],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Agenda",
"config": {
"size": [500, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Agenda",
"config": {
"size": [300, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,91 +0,0 @@
#!python3
"""
inkycal_calendar unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Calendar as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
sample_url = Config.SAMPLE_ICAL_URL
tests = [
{
"name": "Calendar",
"config": {
"size": [500, 500],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Sunday",
"show_events": True,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Monday",
"show_events": False,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": None,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..', end="")
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,69 +0,0 @@
#!python3
"""
inkycal_feeds unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Feeds as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
tests = [
{
"name": "Feeds",
"config": {
"size": [400, 200],
"feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#",
"shuffle_feeds": True,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400, 800],
"feed_urls": "https://www.foodandco.fi/modules/MenuRss/MenuRss/CurrentDay?costNumber=3003&language=en",
"shuffle_feeds": False,
"padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400, 100],
"feed_urls": "https://www.anekdot.ru/rss/export_top.xml",
"shuffle_feeds": False,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
im = merge(im_black, im_colour)
im.show()
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,128 +0,0 @@
#!python3
"""
inkycal_image unittest
"""
import logging
import sys
import unittest
import requests
from PIL import Image
from inkycal.modules import Inkyimage as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
url = "https://github.com/aceinnolab/Inkycal/raw/assets/Repo/coffee.png"
im = Image.open(requests.get(url, stream=True).raw)
im.save("test.png", "PNG")
test_path = "test.png"
tests = [
{
"name": "Inkyimage",
"config": {
"size": [400, 200],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [800, 500],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bw",
"autoflip": False,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "horizontal",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bw",
"autoflip": True,
"orientation": "vertical",
"padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 20, "padding_y": 20, "fontsize": 12, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,70 +0,0 @@
#!python3
"""
inkycal_jokes unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Jokes as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
tests = [
{
"name": "Jokes",
"config": {
"size": [300, 60],
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Jokes",
"config": {
"size": [300, 30],
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Jokes",
"config": {
"size": [100, 800],
"padding_x": 10,
"padding_y": 10,
"fontsize": 18,
"language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,175 +0,0 @@
#!python3
"""
Slideshow test (inkycal_slideshow)
"""
import logging
import os
import sys
import unittest
import requests
from PIL import Image
from inkycal.modules import Slideshow as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
if not os.path.exists("tmp"):
os.mkdir("tmp")
im_urls = [
"https://github.com/aceinnolab/Inkycal/raw/assets/Repo/coffee.png",
"https://github.com/aceinnolab/Inkycal/raw/assets/Repo/coffee.png"
]
for count, url in enumerate(im_urls):
im = Image.open(requests.get(url, stream=True).raw)
im.save(f"tmp/{count}.png", "PNG")
test_path = "tmp"
tests = [
{
"name": "Slideshow",
"config": {
"size": [400, 200],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Slideshow",
"config": {
"size": [800, 500],
"path": test_path,
"palette": "bw",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Slideshow",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwr",
"autoflip": False,
"orientation": "vertical",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Slideshow",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Slideshow",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "horizontal",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Slideshow",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bw",
"autoflip": True,
"orientation": "vertical",
"padding_x": 0,
"padding_y": 0,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Slideshow",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 20,
"padding_y": 20,
"fontsize": 12,
"language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
def test_switch_to_next_image(self):
print(f'testing switching to next images..')
module = Module(tests[0])
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
im_black, im_colour = module.generate_image()
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
print('OK')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,56 +0,0 @@
# #!python3
# """
# inkycal_stocks unittest
# """
# import logging
# import sys
# import unittest
# from inkycal.modules import Stocks as Module
#
# from inkycal.modules.inky_image import Inkyimage
# from inkycal.tests import Config
# preview = Inkyimage.preview
# merge = Inkyimage.merge
#
# tests = [
# {
# "name": "Stocks",
# "config": {
# "size": [528, 30],
# "tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
# "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
# }
# },
# {
# "name": "Stocks",
# "config": {
# "size": [528, 50],
# "tickers": [],
# "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
# }
# }
# ]
#
#
# class module_test(unittest.TestCase):
# def test_get_config(self):
# print('getting data for web-ui...', end="")
# Module.get_config()
# print('OK')
#
# def test_generate_image(self):
# for test in tests:
# print(f'test {tests.index(test) + 1} generating image..')
# module = Module(test)
# im_black, im_colour = module.generate_image()
# print('OK')
# if Config.USE_PREVIEW:
# preview(merge(im_black, im_colour))
#
#
# if __name__ == '__main__':
# logger = logging.getLogger()
# logger.level = logging.DEBUG
# logger.addHandler(logging.StreamHandler(sys.stdout))
#
# unittest.main()

View File

@@ -1,59 +0,0 @@
#!python3
"""
inkycal_todoist unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Todoist as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
api_key = Config.TODOIST_API_KEY
tests = [
{
"name": "Todoist",
"config": {
"size": [400, 1000],
"api_key": api_key,
"project_filter": None,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
if api_key:
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
merge(im_black, im_colour).show()
else:
print('No api key given, omitting test')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,196 +0,0 @@
#!python3
"""
inkycal_weather unittest
"""
import logging
import sys
import unittest
from inkycal.modules import Weather as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
owm_api_key = Config.OPENWEATHERMAP_API_KEY
location = 'Stuttgart, DE'
tests = [
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 100],
"api_key": owm_api_key,
"location": location,
"round_temperature": True,
"round_windspeed": True,
"forecast_interval": "daily",
"units": "metric",
"hour_format": "12",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 150],
"api_key": owm_api_key,
"location": "2643123",
"round_temperature": True,
"round_windspeed": True,
"forecast_interval": "daily",
"units": "metric",
"hour_format": "12",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 200],
"api_key": owm_api_key,
"location": location,
"round_temperature": False,
"round_windspeed": True,
"forecast_interval": "daily",
"units": "metric",
"hour_format": "12",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 100],
"api_key": owm_api_key,
"location": location,
"round_temperature": True,
"round_windspeed": False,
"forecast_interval": "daily",
"units": "metric",
"hour_format": "12",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 150],
"api_key": owm_api_key,
"location": location,
"round_temperature": True,
"round_windspeed": True,
"forecast_interval": "hourly",
"units": "metric",
"hour_format": "12",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 150],
"api_key": owm_api_key,
"location": location,
"round_temperature": True,
"round_windspeed": True,
"forecast_interval": "daily",
"units": "imperial",
"hour_format": "12",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 100],
"api_key": owm_api_key,
"location": location,
"round_temperature": True,
"round_windspeed": True,
"forecast_interval": "daily",
"units": "metric",
"hour_format": "24",
"use_beaufort": True,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Weather",
"config": {
"size": [500, 100],
"api_key": owm_api_key,
"location": location,
"round_temperature": True,
"round_windspeed": True,
"forecast_interval": "daily",
"units": "metric",
"hour_format": "12",
"use_beaufort": False,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()

View File

@@ -1,49 +0,0 @@
{
"model": "epd_7_in_5_v3_colour",
"update_interval": 5,
"orientation": 0,
"info_section": true,
"info_section_height": 70,
"calibration_hours": [0, 12, 18],
"border_around_modules": true,
"modules": [
{
"position": 1,
"name": "Jokes",
"config": {
"size": [528, 80],
"padding_x": 10,"padding_y": 10,"fontsize": 14,"language": "en"
}
},
{
"position": 2,
"name": "Calendar",
"config": {
"size": [
528,
343
],
"week_starts_on": "Monday",
"show_events": true,
"ical_urls": "https://www.officeholidays.com/ics-fed/usa",
"ical_files": null,
"date_format": "D MMM",
"time_format": "HH:mm",
"padding_x": 10,"padding_y": 10,"fontsize": 14,"language": "en"
}
},
{
"position": 3,
"name": "Feeds",
"config": {
"size": [528,132],
"feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#",
"shuffle_feeds": true,
"padding_x": 10,"padding_y": 10,"fontsize": 14,"language": "en"
}
}
]
}

View File

@@ -1,128 +0,0 @@
#!python3
import logging
import os
import sys
import unittest
from inkycal.modules import TextToDisplay as Module
from inkycal.modules.inky_image import Inkyimage
from inkycal.tests import Config
preview = Inkyimage.preview
merge = Inkyimage.merge
file_path = None
dummy_data = [
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', ' Donec feugiat facilisis neque vel blandit.',
'Integer viverra dolor risus.', ' Etiam neque tellus, sollicitudin at nisi a, mollis ornare enim.',
'Quisque sed ante eu leo dictum sagittis quis nec nisi.',
'Suspendisse id nulla dictum, sollicitudin urna id, iaculis elit.',
'Nulla luctus pellentesque diam, ac consequat urna molestie vitae.',
'Donec elementum turpis eget augue laoreet, nec maximus lacus malesuada.', '\n\nEtiam eu nunc mauris.',
'Nullam aliquam tristique leo, at dignissim turpis sodales vitae.',
'Aenean cursus laoreet neque, sit amet semper orci tincidunt et.',
'Proin orci urna, efficitur malesuada mattis at, pretium commodo odio.',
'Maecenas in ante id eros aliquam porttitor quis eget est.',
'Duis ex urna, porta nec semper nec, dignissim eu urna.', ' Quisque eleifend non magna at rutrum.',
'\nSed at eros blandit, tempor quam et, mollis ante.', ' Etiam fringilla euismod gravida.',
'Curabitur facilisis consectetur luctus.',
'Integer lectus augue, convallis a consequat id, sollicitudin eget lorem.',
'Curabitur tincidunt suscipit nibh quis mollis.',
'Fusce cursus, orci ut maximus fringilla, velit mauris dictum est, sed ultricies ante orci viverra erat.',
'Quisque pellentesque, mauris nec vulputate commodo, risus libero volutpat nibh, vel tristique mi neque id quam.',
'\nVivamus blandit, dolor ut interdum sagittis, arcu tortor finibus nibh, ornare convallis dui velit quis nunc.',
'Sed turpis justo, pellentesque eu risus scelerisque, vestibulum vulputate magna.',
'Vivamus tincidunt sollicitudin nisl, feugiat euismod nulla consequat ut.',
'Praesent bibendum, sapien sit amet aliquet posuere, tellus purus porta lectus, ut volutpat purus tellus tempus est.',
'Maecenas condimentum lobortis erat nec dignissim', ' Nulla molestie posuere est',
'Proin ultrices, nisl id luctus lacinia, augue ipsum pharetra leo, quis commodo augue dui varius urna.',
'Morbi ultrices turpis malesuada tellus fermentum vulputate.',
'Aliquam viverra nulla aliquam viverra gravida.', ' Pellentesque eu viverra massa.',
'Vestibulum id nisl vehicula, aliquet dui sed, venenatis eros.',
'Nunc iaculis, neque vitae euismod viverra, nisl mauris luctus velit, a aliquam turpis erat fringilla libero.',
'Ut ligula elit, lacinia convallis tempus interdum, finibus ut ex.',
'Nulla efficitur ac ligula sit amet dignissim.', ' Donec sed mi et justo venenatis faucibus.',
'Sed tincidunt nibh erat, in vestibulum purus consequat eget.',
'\nNulla iaculis volutpat orci id efficitur.', ' Vivamus vehicula sit amet augue tristique dignissim.',
'Praesent eget nulla est.', ' Integer nec diam fermentum, convallis metus lacinia, lobortis mauris.',
'Nulla venenatis metus fringilla, lacinia sem nec, pharetra sapien.',
'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
'Duis facilisis sapien est, a elementum lorem maximus ut.'
]
tests = [
{
"position": 1,
"name": "TextToFile",
"config": {
"size": [500, 100],
"filepath": file_path,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "TextToFile",
"config": {
"size": [500, 400],
"filepath": file_path,
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
]
class TestTextToDisplay(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
delete_file_after_parse = False
if not file_path:
delete_file_after_parse = True
print("Filepath does not exist. Creating dummy file")
tmp_path = "tmp.txt"
with open(tmp_path, mode="w", encoding="utf-8") as file:
file.writelines(dummy_data)
# update tests with new temp path
for test in tests:
test["config"]["filepath"] = tmp_path
else:
make_request = bool(file_path.startswith("https://"))
if not make_request and not os.path.exists(file_path):
raise FileNotFoundError("Your text file could not be found")
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if Config.USE_PREVIEW:
preview(merge(im_black, im_colour))
im = merge(im_black, im_colour)
im.show()
if delete_file_after_parse:
print("cleaning up temp file")
os.remove("tmp.txt")
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()