Initial commit for release v2.0.0
A lot of work-in-progress and far from complete. Lots of improvements related to user-friendliness, fully new web-UI. Better infrastructure.... more coming soon
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
# Settings and Layout
|
||||
from inkycal.config.layout import Layout
|
||||
from inkycal.config.settings_parser import Settings
|
||||
#from inkycal.config.layout import Layout
|
||||
#from inkycal.config.settings_parser import Settings
|
||||
from inkycal.display import Display
|
||||
|
||||
# All supported inkycal_modules
|
||||
import inkycal.modules.inkycal_agenda
|
||||
import inkycal.modules.inkycal_calendar
|
||||
import inkycal.modules.inkycal_weather
|
||||
import inkycal.modules.inkycal_rss
|
||||
# import inkycal.modules.inkycal_image
|
||||
#import inkycal.modules.inkycal_image
|
||||
# import inkycal.modules.inkycal_server
|
||||
|
||||
# Main file
|
||||
from inkycal.main import Inkycal
|
||||
|
||||
# Added by module adder
|
||||
|
474
inkycal/backup.py
Normal file
474
inkycal/backup.py
Normal file
@@ -0,0 +1,474 @@
|
||||
from inkycal import Settings, Layout
|
||||
from inkycal.custom import *
|
||||
|
||||
#from os.path import exists
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
import arrow
|
||||
import time
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print('Pillow is not installed! Please install with:')
|
||||
print('pip3 install Pillow')
|
||||
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
print('numpy is not installed! Please install with:')
|
||||
print('pip3 install numpy')
|
||||
|
||||
logger = logging.getLogger('inkycal')
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
class Inkycal:
|
||||
"""Inkycal main class"""
|
||||
|
||||
def __init__(self, settings_path, render=True):
|
||||
"""Initialise Inkycal
|
||||
settings_path = str -> location/folder of settings file
|
||||
render = bool -> show something on the ePaper?
|
||||
"""
|
||||
self._release = '2.0.0beta'
|
||||
|
||||
# Check if render is boolean
|
||||
if not isinstance(render, bool):
|
||||
raise Exception('render must be True or False, not "{}"'.format(render))
|
||||
self.render = render
|
||||
|
||||
# Init settings class
|
||||
self.Settings = Settings(settings_path)
|
||||
|
||||
# Check if display support colour
|
||||
self.supports_colour = self.Settings.Layout.supports_colour
|
||||
|
||||
# Option to flip image upside down
|
||||
if self.Settings.display_orientation == 'normal':
|
||||
self.upside_down = False
|
||||
|
||||
elif self.Settings.display_orientation == 'upside_down':
|
||||
self.upside_down = True
|
||||
|
||||
# Option to use epaper image optimisation
|
||||
self.optimize = True
|
||||
|
||||
# Load drivers if image should be rendered
|
||||
if self.render == True:
|
||||
|
||||
# Get model and check if colour can be rendered
|
||||
model= self.Settings.model
|
||||
|
||||
# Init Display class
|
||||
from inkycal.display import Display
|
||||
self.Display = Display(model)
|
||||
|
||||
# get calibration hours
|
||||
self._calibration_hours = self.Settings.calibration_hours
|
||||
|
||||
# set a check for calibration
|
||||
self._calibration_state = False
|
||||
|
||||
# load+validate settings file. Import and setup specified modules
|
||||
self.active_modules = self.Settings.active_modules()
|
||||
for module in self.active_modules:
|
||||
try:
|
||||
loader = 'from inkycal.modules import {0}'.format(module)
|
||||
module_data = self.Settings.get_config(module)
|
||||
size, conf = module_data['size'], module_data['config']
|
||||
setup = 'self.{} = {}(size, conf)'.format(module, module)
|
||||
exec(loader)
|
||||
exec(setup)
|
||||
logger.debug(('{}: size: {}, config: {}'.format(module, size, conf)))
|
||||
|
||||
# If a module was not found, print an error message
|
||||
except ImportError:
|
||||
print(
|
||||
'Could not find module: "{}". Please try to import manually.'.format(
|
||||
module))
|
||||
|
||||
# Give an OK message
|
||||
print('loaded inkycal')
|
||||
|
||||
def countdown(self, interval_mins=None):
|
||||
"""Returns the remaining time in seconds until next display update"""
|
||||
|
||||
# Validate update interval
|
||||
allowed_intervals = [10, 15, 20, 30, 60]
|
||||
|
||||
# Check if empty, if empty, use value from settings file
|
||||
if interval_mins == None:
|
||||
interval_mins = self.Settings.update_interval
|
||||
|
||||
# Check if integer
|
||||
if not isinstance(interval_mins, int):
|
||||
raise Exception('Update interval must be an integer -> 60')
|
||||
|
||||
# Check if value is supported
|
||||
if interval_mins not in allowed_intervals:
|
||||
raise Exception('Update interval is {}, but should be one of: {}'.format(
|
||||
interval_mins, allowed_intervals))
|
||||
|
||||
# Find out at which minutes the update should happen
|
||||
now = arrow.now()
|
||||
update_timings = [(60 - int(interval_mins)*updates) for updates in
|
||||
range(60//int(interval_mins))][::-1]
|
||||
|
||||
# Calculate time in mins until next update
|
||||
minutes = [_ for _ in update_timings if _>= now.minute][0] - now.minute
|
||||
|
||||
# Print the remaining time in mins until next update
|
||||
print('{0} Minutes left until next refresh'.format(minutes))
|
||||
|
||||
# Calculate time in seconds until next update
|
||||
remaining_time = minutes*60 + (60 - now.second)
|
||||
|
||||
# Return seconds until next update
|
||||
return remaining_time
|
||||
|
||||
def test(self):
|
||||
"""Inkycal test run.
|
||||
Generates images for each module, one by one and prints OK if no
|
||||
problems were found."""
|
||||
print('You are running inkycal v{}'.format(self._release))
|
||||
|
||||
|
||||
print('Running inkycal test-run for {} ePaper'.format(
|
||||
self.Settings.model))
|
||||
|
||||
if self.upside_down == True:
|
||||
print('upside-down mode active')
|
||||
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
print('generating image for {} module...'.format(module), end = '')
|
||||
try:
|
||||
exec(generate_im)
|
||||
print('OK!')
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
print(traceback.format_exc())
|
||||
|
||||
def run(self):
|
||||
"""Runs the main inykcal program nonstop (cannot be stopped anymore!)
|
||||
Will show something on the display if render was set to True"""
|
||||
|
||||
# TODO: printing traceback on display (or at least a smaller message?)
|
||||
# Calibration
|
||||
|
||||
# Get the time of initial run
|
||||
runtime = arrow.now()
|
||||
|
||||
# Function to flip images upside down
|
||||
upside_down = lambda image: image.rotate(180, expand=True)
|
||||
|
||||
# Count the number of times without any errors
|
||||
counter = 1
|
||||
|
||||
# Calculate the max. fontsize for info-section
|
||||
if self.Settings.info_section == True:
|
||||
info_section_height = round(self.Settings.Layout.display_height* (1/95) )
|
||||
self.font = auto_fontsize(ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed']), info_section_height)
|
||||
|
||||
while True:
|
||||
print('Generating images for all modules...')
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
try:
|
||||
exec(generate_im)
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
message = traceback.format_exc()
|
||||
print(message)
|
||||
counter = 0
|
||||
print('OK')
|
||||
|
||||
# Assemble image from each module
|
||||
self._assemble()
|
||||
|
||||
# Check if image should be rendered
|
||||
if self.render == True:
|
||||
Display = self.Display
|
||||
|
||||
self._calibration_check()
|
||||
|
||||
if self.supports_colour == True:
|
||||
im_black = Image.open(images+'canvas.png')
|
||||
im_colour = Image.open(images+'canvas_colour.png')
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.upside_down == True:
|
||||
im_black = upside_down(im_black)
|
||||
im_colour = upside_down(im_colour)
|
||||
|
||||
# render the image on the display
|
||||
Display.render(im_black, im_colour)
|
||||
|
||||
# Part for black-white ePapers
|
||||
elif self.supports_colour == False:
|
||||
|
||||
im_black = self._merge_bands()
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.upside_down == True:
|
||||
im_black = upside_down(im_black)
|
||||
|
||||
Display.render(im_black)
|
||||
|
||||
print('\ninkycal has been running without any errors for', end = ' ')
|
||||
print('{} display updates'.format(counter))
|
||||
print('Programm started {}'.format(runtime.humanize()))
|
||||
|
||||
counter += 1
|
||||
|
||||
sleep_time = self.countdown()
|
||||
time.sleep(sleep_time)
|
||||
|
||||
def _merge_bands(self):
|
||||
"""Merges black and coloured bands for black-white ePapers
|
||||
returns the merged image
|
||||
"""
|
||||
|
||||
im_path = images
|
||||
|
||||
im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png'
|
||||
|
||||
# If there is an image for black and colour, merge them
|
||||
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
||||
|
||||
im1 = Image.open(im1_path).convert('RGBA')
|
||||
im2 = Image.open(im2_path).convert('RGBA')
|
||||
|
||||
def clear_white(img):
|
||||
"""Replace all white pixels from image with transparent pixels
|
||||
"""
|
||||
x = numpy.asarray(img.convert('RGBA')).copy()
|
||||
x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8)
|
||||
return Image.fromarray(x)
|
||||
|
||||
im2 = clear_white(im2)
|
||||
im1.paste(im2, (0,0), im2)
|
||||
|
||||
# If there is no image for the coloured-band, return the bw-image
|
||||
elif os.path.exists(im1_path) and not os.path.exists(im2_path):
|
||||
im1 = Image.open(im1_name).convert('RGBA')
|
||||
|
||||
return im1
|
||||
|
||||
|
||||
def _assemble(self):
|
||||
"""Assmebles all sub-images to a single image"""
|
||||
|
||||
# Create an empty canvas with the size of the display
|
||||
width, height = self.Settings.Layout.display_size
|
||||
|
||||
if self.Settings.info_section == True:
|
||||
height = round(height * ((1/95)*100) )
|
||||
|
||||
im_black = Image.new('RGB', (width, height), color = 'white')
|
||||
im_colour = Image.new('RGB', (width ,height), color = 'white')
|
||||
|
||||
# Set cursor for y-axis
|
||||
im1_cursor = 0
|
||||
im2_cursor = 0
|
||||
|
||||
for module in self.active_modules:
|
||||
|
||||
im1_path = images+module+'.png'
|
||||
im2_path = images+module+'_colour.png'
|
||||
|
||||
# Check if there is an image for the black band
|
||||
if os.path.exists(im1_path):
|
||||
|
||||
# Get actual size of image
|
||||
im1 = Image.open(im1_path).convert('RGBA')
|
||||
im1_size = im1.size
|
||||
|
||||
# Get the size of the section
|
||||
section_size = self.Settings.get_config(module)['size']
|
||||
# Calculate coordinates to center the image
|
||||
x = int( (section_size[0] - im1_size[0]) /2)
|
||||
|
||||
# If this is the first module, use the y-offset
|
||||
if im1_cursor == 0:
|
||||
y = int( (section_size[1]-im1_size[1]) /2)
|
||||
else:
|
||||
y = im1_cursor + int( (section_size[1]-im1_size[1]) /2)
|
||||
|
||||
# center the image in the section space
|
||||
im_black.paste(im1, (x,y), im1)
|
||||
|
||||
# Shift the y-axis cursor at the beginning of next section
|
||||
im1_cursor += section_size[1]
|
||||
|
||||
# Check if there is an image for the coloured band
|
||||
if os.path.exists(im2_path):
|
||||
|
||||
# Get actual size of image
|
||||
im2 = Image.open(im2_path).convert('RGBA')
|
||||
im2_size = im2.size
|
||||
|
||||
# Get the size of the section
|
||||
section_size = self.Settings.get_config(module)['size']
|
||||
|
||||
# Calculate coordinates to center the image
|
||||
x = int( (section_size[0]-im2_size[0]) /2)
|
||||
|
||||
# If this is the first module, use the y-offset
|
||||
if im2_cursor == 0:
|
||||
y = int( (section_size[1]-im2_size[1]) /2)
|
||||
else:
|
||||
y = im2_cursor + int( (section_size[1]-im2_size[1]) /2)
|
||||
|
||||
# center the image in the section space
|
||||
im_colour.paste(im2, (x,y), im2)
|
||||
|
||||
# Shift the y-axis cursor at the beginning of next section
|
||||
im2_cursor += section_size[1]
|
||||
|
||||
# Show an info section if specified by the settings file
|
||||
now = arrow.now()
|
||||
stamp = 'last update: {}'.format(now.format('D MMM @ HH:mm', locale =
|
||||
self.Settings.language))
|
||||
if self.Settings.info_section == True:
|
||||
write(im_black, (0, im1_cursor), (width, height-im1_cursor),
|
||||
stamp, font = self.font)
|
||||
|
||||
|
||||
# optimize the image by mapping colours to pure black and white
|
||||
if self.optimize == True:
|
||||
self._optimize_im(im_black).save(images+'canvas.png', 'PNG')
|
||||
self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG')
|
||||
else:
|
||||
im_black.save(images+'canvas.png', 'PNG')
|
||||
im_colour.save(images+'canvas_colour.png', 'PNG')
|
||||
|
||||
def _optimize_im(self, image, threshold=220):
|
||||
"""Optimize the image for rendering on ePaper displays"""
|
||||
|
||||
buffer = numpy.array(image.convert('RGB'))
|
||||
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
||||
# grey->black
|
||||
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0]
|
||||
image = Image.fromarray(buffer)
|
||||
return image
|
||||
|
||||
def calibrate(self):
|
||||
"""Calibrate the ePaper display to prevent burn-ins (ghosting)
|
||||
use this command to manually calibrate the display"""
|
||||
|
||||
self.Display.calibrate()
|
||||
|
||||
def _calibration_check(self):
|
||||
"""Calibration sheduler
|
||||
uses calibration hours from settings file to check if calibration is due"""
|
||||
now = arrow.now()
|
||||
print('hour:', now.hour, 'hours:', self._calibration_hours)
|
||||
print('state:', self._calibration_state)
|
||||
if now.hour in self._calibration_hours and self._calibration_state == False:
|
||||
self.calibrate()
|
||||
self._calibration_state = True
|
||||
else:
|
||||
self._calibration_state = False
|
||||
|
||||
|
||||
def _check_for_updates(self):
|
||||
"""Check if a new update is available for inkycal"""
|
||||
|
||||
raise NotImplementedError('Tha developer were too lazy to implement this..')
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _add_module(filepath_module, classname):
|
||||
"""Add a third party module to inkycal
|
||||
filepath_module = the full path of your module. The file should be in /modules!
|
||||
classname = the name of your class inside the module
|
||||
"""
|
||||
|
||||
# Path for modules
|
||||
_module_path = 'inkycal/modules/'
|
||||
|
||||
# Check if the filepath is a string
|
||||
if not isinstance(filepath_module, str):
|
||||
raise ValueError('filepath has to be a string!')
|
||||
|
||||
# Check if the classname is a string
|
||||
if not isinstance(classname, str):
|
||||
raise ValueError('classname has to be a string!')
|
||||
|
||||
# TODO:
|
||||
# Ensure only third-party modules are deleted as built-in modules
|
||||
# should not be deleted
|
||||
|
||||
# Check if module is inside the modules folder
|
||||
if not _module_path in filepath_module:
|
||||
raise Exception('Your module should be in', _module_path)
|
||||
|
||||
# Get the name of the third-party module file without extension (.py)
|
||||
filename = filepath_module.split('.py')[0].split('/')[-1]
|
||||
|
||||
# Check if filename or classname is in the current module init file
|
||||
with open('modules/__init__.py', mode ='r') as module_init:
|
||||
content = module_init.read().splitlines()
|
||||
|
||||
for line in content:
|
||||
if (filename or clasname) in line:
|
||||
raise Exception(
|
||||
'A module with this filename or classname already exists')
|
||||
|
||||
# Check if filename or classname is in the current inkycal init file
|
||||
with open('__init__.py', mode ='r') as inkycal_init:
|
||||
content = inkycal_init.read().splitlines()
|
||||
|
||||
for line in content:
|
||||
if (filename or clasname) in line:
|
||||
raise Exception(
|
||||
'A module with this filename or classname already exists')
|
||||
|
||||
# If all checks have passed, add the module in the module init file
|
||||
with open('modules/__init__.py', mode='a') as module_init:
|
||||
module_init.write('from .{} import {}'.format(filename, classname))
|
||||
|
||||
# If all checks have passed, add the module in the inkycal init file
|
||||
with open('__init__.py', mode ='a') as inkycal_init:
|
||||
inkycal_init.write('# Added by module adder \n')
|
||||
inkycal_init.write('import inkycal.modules.{}'.format(filename))
|
||||
|
||||
print('Your module {} has been added successfully! Hooray!'.format(
|
||||
classname))
|
||||
|
||||
@staticmethod
|
||||
def _remove_module(classname, remove_file = True):
|
||||
"""Removes a third-party module from inkycal
|
||||
Input the classname of the file you want to remove
|
||||
"""
|
||||
|
||||
# Check if filename or classname is in the current module init file
|
||||
with open('modules/__init__.py', mode ='r') as module_init:
|
||||
content = module_init.read().splitlines()
|
||||
|
||||
with open('modules/__init__.py', mode ='w') as module_init:
|
||||
for line in content:
|
||||
if not classname in line:
|
||||
module_init.write(line+'\n')
|
||||
else:
|
||||
filename = line.split(' ')[1].split('.')[1]
|
||||
|
||||
# Check if filename or classname is in the current inkycal init file
|
||||
with open('__init__.py', mode ='r') as inkycal_init:
|
||||
content = inkycal_init.read().splitlines()
|
||||
|
||||
with open('__init__.py', mode ='w') as inkycal_init:
|
||||
for line in content:
|
||||
if not filename in line:
|
||||
inkycal_init.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('modules/{}.py'.format(filename)) and remove_file == True:
|
||||
os.remove('modules/{}.py'.format(filename))
|
||||
|
||||
print('The module {} has been removed successfully'.format(classname))
|
||||
|
@@ -24,8 +24,6 @@ class Layout:
|
||||
if (model != None) and (width == None) and (height == None):
|
||||
display_dimensions = {
|
||||
'9_in_7': (1200, 825),
|
||||
'epd_7_in_5_v3_colour': (880, 528),
|
||||
'epd_7_in_5_v3': (880, 528),
|
||||
'epd_7_in_5_v2_colour': (800, 480),
|
||||
'epd_7_in_5_v2': (800, 480),
|
||||
'epd_7_in_5_colour': (640, 384),
|
||||
@@ -116,6 +114,14 @@ class Layout:
|
||||
size = (self.bottom_section_width, self.bottom_section_height)
|
||||
return size
|
||||
|
||||
## def set_info_section(self, value):
|
||||
## """Should a small info section be showed """
|
||||
## if not isinstance(value, bool):
|
||||
## raise ValueError('value has to bee a boolean: True/False')
|
||||
## self.info_section = value
|
||||
## logger.info(('show info section: {}').format(value))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(
|
||||
os.path.basename(__file__).split('.py')[0]))
|
||||
|
@@ -23,7 +23,6 @@ class Settings:
|
||||
_supported_update_interval = [10, 15, 20, 30, 60]
|
||||
_supported_display_orientation = ['normal', 'upside_down']
|
||||
_supported_models = [
|
||||
'epd_7_in_5_v3_colour', 'epd_7_in_5_v3',
|
||||
'epd_7_in_5_v2_colour', 'epd_7_in_5_v2',
|
||||
'epd_7_in_5_colour', 'epd_7_in_5',
|
||||
'epd_5_in_83_colour','epd_5_in_83',
|
||||
|
@@ -67,7 +67,7 @@ def auto_fontsize(font, max_height):
|
||||
def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
"""Write text on specified image
|
||||
image = on which image should the text be added?
|
||||
xy = xy-coordinates as tuple -> (x,y)
|
||||
xy = (x,y) coordinates as tuple -> (x,y)
|
||||
box_size = size of text-box -> (width,height)
|
||||
text = string (what to write)
|
||||
font = which font to use
|
||||
|
@@ -1 +1 @@
|
||||
from .epaper import Display
|
||||
from .display import Display
|
||||
|
130
inkycal/display/display.py
Normal file
130
inkycal/display/display.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Inky-Calendar epaper functions
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from importlib import import_module
|
||||
from PIL import Image
|
||||
|
||||
from inkycal.custom import top_level
|
||||
import glob
|
||||
|
||||
class Display:
|
||||
"""Display class for inkycal
|
||||
Handles rendering on display"""
|
||||
|
||||
def __init__(self, epaper_model):
|
||||
"""Load the drivers for this epaper model"""
|
||||
|
||||
if 'colour' in epaper_model:
|
||||
self.supports_colour = True
|
||||
else:
|
||||
self.supports_colour = False
|
||||
|
||||
try:
|
||||
driver_path = f'inkycal.display.drivers.{epaper_model}'
|
||||
driver = import_module(driver_path)
|
||||
self._epaper = driver.EPD()
|
||||
self.model_name = epaper_model
|
||||
#self.height = driver.EPD_HEIGHT
|
||||
#self.width = driver.EPD_WIDTH
|
||||
|
||||
except ImportError:
|
||||
raise Exception('This module is not supported. Check your spellings?')
|
||||
|
||||
except FileNotFoundError:
|
||||
raise Exception('SPI could not be found. Please check if SPI is enabled')
|
||||
|
||||
def render(self, im_black, im_colour = None):
|
||||
"""Render an image on the epaper
|
||||
im_colour is required for three-colour epapers"""
|
||||
|
||||
epaper = self._epaper
|
||||
|
||||
if self.supports_colour == False:
|
||||
print('Initialising..', end = '')
|
||||
epaper.init()
|
||||
# For the 9.7" ePaper, the image needs to be flipped by 90 deg first
|
||||
# The other displays flip the image automatically
|
||||
if self.model_name == "9_in_7":
|
||||
im_black.rotate(90, expand=True)
|
||||
print('Updating display......', end = '')
|
||||
epaper.display(epaper.getbuffer(im_black))
|
||||
print('Done')
|
||||
|
||||
elif self.supports_colour == True:
|
||||
if not im_colour:
|
||||
raise Exception('im_colour is required for coloured epaper displays')
|
||||
print('Initialising..', end = '')
|
||||
epaper.init()
|
||||
print('Updating display......', end = '')
|
||||
epaper.display(epaper.getbuffer(im_black), epaper.getbuffer(im_colour))
|
||||
print('Done')
|
||||
|
||||
print('Sending E-Paper to deep sleep...', end = '')
|
||||
epaper.sleep()
|
||||
print('Done')
|
||||
|
||||
def calibrate(self, cycles=3):
|
||||
"""Flush display with single colour to prevent burn-ins (ghosting)
|
||||
cycles -> int. How many times should each colour be flushed?
|
||||
recommended cycles = 3"""
|
||||
|
||||
epaper = self._epaper
|
||||
epaper.init()
|
||||
|
||||
white = Image.new('1', (epaper.width, epaper.height), 'white')
|
||||
black = Image.new('1', (epaper.width, epaper.height), 'black')
|
||||
|
||||
print('----------Started calibration of ePaper display----------')
|
||||
if self.supports_colour == True:
|
||||
for _ in range(cycles):
|
||||
print('Calibrating...', end= ' ')
|
||||
print('black...', end= ' ')
|
||||
epaper.display(epaper.getbuffer(black), epaper.getbuffer(white))
|
||||
print('colour...', end = ' ')
|
||||
epaper.display(epaper.getbuffer(white), epaper.getbuffer(black))
|
||||
print('white...')
|
||||
epaper.display(epaper.getbuffer(white), epaper.getbuffer(white))
|
||||
print('Cycle {0} of {1} complete'.format(_+1, cycles))
|
||||
|
||||
if self.supports_colour == False:
|
||||
for _ in range(cycles):
|
||||
print('Calibrating...', end= ' ')
|
||||
print('black...', end = ' ')
|
||||
epaper.display(epaper.getbuffer(black))
|
||||
print('white...')
|
||||
epaper.display(epaper.getbuffer(white)),
|
||||
print('Cycle {0} of {1} complete'.format(_+1, cycles))
|
||||
|
||||
print('-----------Calibration complete----------')
|
||||
epaper.sleep()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_display_size(cls, model_name):
|
||||
"returns (width, height) of given display"
|
||||
if not isinstance(model_name, str):
|
||||
print('model_name should be a string')
|
||||
return
|
||||
else:
|
||||
driver_files = top_level+'/inkycal/display/drivers/*.py'
|
||||
drivers = glob.glob(driver_files)
|
||||
drivers = [i.split('/')[-1].split('.')[0] for i in drivers]
|
||||
if model_name not in drivers:
|
||||
print('This model name was not found. Please double check your spellings')
|
||||
return
|
||||
else:
|
||||
with open(top_level+'/inkycal/display/drivers/'+model_name+'.py') as file:
|
||||
for line in file:
|
||||
if 'EPD_WIDTH=' in line.replace(" ", ""):
|
||||
width = int(line.rstrip().replace(" ", "").split('=')[-1])
|
||||
if 'EPD_HEIGHT=' in line.replace(" ", ""):
|
||||
height = int(line.rstrip().replace(" ", "").split('=')[-1])
|
||||
return width, height
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Running Display class in standalone mode")
|
||||
|
413
inkycal/main.py
413
inkycal/main.py
@@ -1,12 +1,19 @@
|
||||
from inkycal import Settings, Layout
|
||||
from inkycal.custom import *
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#from os.path import exists
|
||||
"""
|
||||
Main class for inkycal Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
from inkycal.display import Display
|
||||
from inkycal.custom import *
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
import arrow
|
||||
import time
|
||||
import json
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
@@ -20,36 +27,36 @@ except ImportError:
|
||||
print('numpy is not installed! Please install with:')
|
||||
print('pip3 install numpy')
|
||||
|
||||
logger = logging.getLogger('inkycal')
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
|
||||
class Inkycal:
|
||||
"""Inkycal main class"""
|
||||
|
||||
def __init__(self, settings_path, render=True):
|
||||
"""initialise class
|
||||
"""Initialise Inkycal
|
||||
settings_path = str -> location/folder of settings file
|
||||
render = bool -> show something on the ePaper?
|
||||
"""
|
||||
self._release = '2.0.0beta'
|
||||
self._release = '2.0.0'
|
||||
|
||||
# Check if render is boolean
|
||||
if not isinstance(render, bool):
|
||||
# Check if render was set correctly
|
||||
if render not in [True, False]:
|
||||
raise Exception('render must be True or False, not "{}"'.format(render))
|
||||
self.render = render
|
||||
|
||||
# Init settings class
|
||||
self.Settings = Settings(settings_path)
|
||||
# load settings file - throw an error if file could not be found
|
||||
try:
|
||||
with open(settings_path) as file:
|
||||
settings = json.load(file)
|
||||
self.settings = settings
|
||||
#print(self.settings)
|
||||
|
||||
# Check if display support colour
|
||||
self.supports_colour = self.Settings.Layout.supports_colour
|
||||
|
||||
# Option to flip image upside down
|
||||
if self.Settings.display_orientation == 'normal':
|
||||
self.upside_down = False
|
||||
|
||||
elif self.Settings.display_orientation == 'upside_down':
|
||||
self.upside_down = True
|
||||
except FileNotFoundError:
|
||||
print('No settings file found in specified location')
|
||||
print('Please double check your path')
|
||||
|
||||
# Option to use epaper image optimisation
|
||||
self.optimize = True
|
||||
@@ -57,27 +64,26 @@ class Inkycal:
|
||||
# Load drivers if image should be rendered
|
||||
if self.render == True:
|
||||
|
||||
# Get model and check if colour can be rendered
|
||||
model= self.Settings.model
|
||||
|
||||
# Init Display class
|
||||
# Init Display class with model in settings file
|
||||
from inkycal.display import Display
|
||||
self.Display = Display(model)
|
||||
self.Display = Display(settings["model"])
|
||||
|
||||
# get calibration hours
|
||||
self._calibration_hours = self.Settings.calibration_hours
|
||||
# check if colours can be rendered
|
||||
self.supports_colour = True if 'colour' in settings['model'] else False
|
||||
|
||||
# set a check for calibration
|
||||
# init calibration state
|
||||
self._calibration_state = False
|
||||
|
||||
# load+validate settings file. Import and setup specified modules
|
||||
self.active_modules = self.Settings.active_modules()
|
||||
for module in self.active_modules:
|
||||
|
||||
|
||||
# WIP
|
||||
for module in settings['modules']:
|
||||
try:
|
||||
loader = 'from inkycal.modules import {0}'.format(module)
|
||||
module_data = self.Settings.get_config(module)
|
||||
size, conf = module_data['size'], module_data['config']
|
||||
setup = 'self.{} = {}(size, conf)'.format(module, module)
|
||||
loader = f'from inkycal.modules import {module["name"]}'
|
||||
print(loader)
|
||||
conf = module["config"]
|
||||
#size, conf = module_data['size'], module_data['config']
|
||||
setup = f'self.{module} = {module}(size, conf)'
|
||||
exec(loader)
|
||||
exec(setup)
|
||||
logger.debug(('{}: size: {}, config: {}'.format(module, size, conf)))
|
||||
@@ -88,6 +94,9 @@ class Inkycal:
|
||||
'Could not find module: "{}". Please try to import manually.'.format(
|
||||
module))
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
# Give an OK message
|
||||
print('loaded inkycal')
|
||||
|
||||
@@ -99,7 +108,7 @@ class Inkycal:
|
||||
|
||||
# Check if empty, if empty, use value from settings file
|
||||
if interval_mins == None:
|
||||
interval_mins = self.Settings.update_interval
|
||||
interval_mins = self.settings.update_interval
|
||||
|
||||
# Check if integer
|
||||
if not isinstance(interval_mins, int):
|
||||
@@ -127,348 +136,18 @@ class Inkycal:
|
||||
# Return seconds until next update
|
||||
return remaining_time
|
||||
|
||||
def test(self):
|
||||
"""Inkycal test run.
|
||||
Generates images for each module, one by one and prints OK if no
|
||||
problems were found."""
|
||||
print('You are running inkycal v{}'.format(self._release))
|
||||
|
||||
|
||||
print('Running inkycal test-run for {} ePaper'.format(
|
||||
self.Settings.model))
|
||||
|
||||
if self.upside_down == True:
|
||||
print('upside-down mode active')
|
||||
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
print('generating image for {} module...'.format(module), end = '')
|
||||
try:
|
||||
exec(generate_im)
|
||||
print('OK!')
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
print(traceback.format_exc())
|
||||
|
||||
def run(self):
|
||||
"""Runs the main inykcal program nonstop (cannot be stopped anymore!)
|
||||
Will show something on the display if render was set to True"""
|
||||
|
||||
# TODO: printing traceback on display (or at least a smaller message?)
|
||||
# Calibration
|
||||
|
||||
# Get the time of initial run
|
||||
runtime = arrow.now()
|
||||
|
||||
# Function to flip images upside down
|
||||
upside_down = lambda image: image.rotate(180, expand=True)
|
||||
|
||||
# Count the number of times without any errors
|
||||
counter = 1
|
||||
|
||||
# Calculate the max. fontsize for info-section
|
||||
if self.Settings.info_section == True:
|
||||
info_section_height = round(self.Settings.Layout.display_height* (1/95) )
|
||||
self.font = auto_fontsize(ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed']), info_section_height)
|
||||
|
||||
while True:
|
||||
print('Generating images for all modules...')
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
try:
|
||||
exec(generate_im)
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
message = traceback.format_exc()
|
||||
print(message)
|
||||
counter = 0
|
||||
print('OK')
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
|
||||
# Assemble image from each module
|
||||
self._assemble()
|
||||
|
||||
# Check if image should be rendered
|
||||
if self.render == True:
|
||||
Display = self.Display
|
||||
|
||||
self._calibration_check()
|
||||
|
||||
if self.supports_colour == True:
|
||||
im_black = Image.open(images+'canvas.png')
|
||||
im_colour = Image.open(images+'canvas_colour.png')
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.upside_down == True:
|
||||
im_black = upside_down(im_black)
|
||||
im_colour = upside_down(im_colour)
|
||||
|
||||
# render the image on the display
|
||||
Display.render(im_black, im_colour)
|
||||
|
||||
# Part for black-white ePapers
|
||||
elif self.supports_colour == False:
|
||||
|
||||
im_black = self._merge_bands()
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.upside_down == True:
|
||||
im_black = upside_down(im_black)
|
||||
|
||||
Display.render(im_black)
|
||||
|
||||
print('\ninkycal has been running without any errors for', end = ' ')
|
||||
print('{} display updates'.format(counter))
|
||||
print('Programm started {}'.format(runtime.humanize()))
|
||||
|
||||
counter += 1
|
||||
|
||||
sleep_time = self.countdown()
|
||||
time.sleep(sleep_time)
|
||||
|
||||
def _merge_bands(self):
|
||||
"""Merges black and coloured bands for black-white ePapers
|
||||
returns the merged image
|
||||
"""
|
||||
|
||||
im_path = images
|
||||
|
||||
im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png'
|
||||
|
||||
# If there is an image for black and colour, merge them
|
||||
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
||||
|
||||
im1 = Image.open(im1_path).convert('RGBA')
|
||||
im2 = Image.open(im2_path).convert('RGBA')
|
||||
|
||||
def clear_white(img):
|
||||
"""Replace all white pixels from image with transparent pixels
|
||||
"""
|
||||
x = numpy.asarray(img.convert('RGBA')).copy()
|
||||
x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8)
|
||||
return Image.fromarray(x)
|
||||
|
||||
im2 = clear_white(im2)
|
||||
im1.paste(im2, (0,0), im2)
|
||||
|
||||
# If there is no image for the coloured-band, return the bw-image
|
||||
elif os.path.exists(im1_path) and not os.path.exists(im2_path):
|
||||
im1 = Image.open(im1_name).convert('RGBA')
|
||||
|
||||
return im1
|
||||
|
||||
|
||||
def _assemble(self):
|
||||
"""Assmebles all sub-images to a single image"""
|
||||
|
||||
# Create an empty canvas with the size of the display
|
||||
width, height = self.Settings.Layout.display_size
|
||||
|
||||
if self.Settings.info_section == True:
|
||||
height = round(height * ((1/95)*100) )
|
||||
|
||||
im_black = Image.new('RGB', (width, height), color = 'white')
|
||||
im_colour = Image.new('RGB', (width ,height), color = 'white')
|
||||
|
||||
# Set cursor for y-axis
|
||||
im1_cursor = 0
|
||||
im2_cursor = 0
|
||||
|
||||
for module in self.active_modules:
|
||||
|
||||
im1_path = images+module+'.png'
|
||||
im2_path = images+module+'_colour.png'
|
||||
|
||||
# Check if there is an image for the black band
|
||||
if os.path.exists(im1_path):
|
||||
|
||||
# Get actual size of image
|
||||
im1 = Image.open(im1_path).convert('RGBA')
|
||||
im1_size = im1.size
|
||||
|
||||
# Get the size of the section
|
||||
section_size = self.Settings.get_config(module)['size']
|
||||
# Calculate coordinates to center the image
|
||||
x = int( (section_size[0] - im1_size[0]) /2)
|
||||
|
||||
# If this is the first module, use the y-offset
|
||||
if im1_cursor == 0:
|
||||
y = int( (section_size[1]-im1_size[1]) /2)
|
||||
else:
|
||||
y = im1_cursor + int( (section_size[1]-im1_size[1]) /2)
|
||||
|
||||
# center the image in the section space
|
||||
im_black.paste(im1, (x,y), im1)
|
||||
|
||||
# Shift the y-axis cursor at the beginning of next section
|
||||
im1_cursor += section_size[1]
|
||||
|
||||
# Check if there is an image for the coloured band
|
||||
if os.path.exists(im2_path):
|
||||
|
||||
# Get actual size of image
|
||||
im2 = Image.open(im2_path).convert('RGBA')
|
||||
im2_size = im2.size
|
||||
|
||||
# Get the size of the section
|
||||
section_size = self.Settings.get_config(module)['size']
|
||||
|
||||
# Calculate coordinates to center the image
|
||||
x = int( (section_size[0]-im2_size[0]) /2)
|
||||
|
||||
# If this is the first module, use the y-offset
|
||||
if im2_cursor == 0:
|
||||
y = int( (section_size[1]-im2_size[1]) /2)
|
||||
else:
|
||||
y = im2_cursor + int( (section_size[1]-im2_size[1]) /2)
|
||||
|
||||
# center the image in the section space
|
||||
im_colour.paste(im2, (x,y), im2)
|
||||
|
||||
# Shift the y-axis cursor at the beginning of next section
|
||||
im2_cursor += section_size[1]
|
||||
|
||||
# Show an info section if specified by the settings file
|
||||
now = arrow.now()
|
||||
stamp = 'last update: {}'.format(now.format('D MMM @ HH:mm', locale =
|
||||
self.Settings.language))
|
||||
if self.Settings.info_section == True:
|
||||
write(im_black, (0, im1_cursor), (width, height-im1_cursor),
|
||||
stamp, font = self.font)
|
||||
|
||||
|
||||
# optimize the image by mapping colours to pure black and white
|
||||
if self.optimize == True:
|
||||
self._optimize_im(im_black).save(images+'canvas.png', 'PNG')
|
||||
self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG')
|
||||
else:
|
||||
im_black.save(images+'canvas.png', 'PNG')
|
||||
im_colour.save(images+'canvas_colour.png', 'PNG')
|
||||
|
||||
def _optimize_im(self, image, threshold=220):
|
||||
"""Optimize the image for rendering on ePaper displays"""
|
||||
|
||||
buffer = numpy.array(image.convert('RGB'))
|
||||
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
||||
# grey->black
|
||||
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0]
|
||||
image = Image.fromarray(buffer)
|
||||
return image
|
||||
|
||||
def calibrate(self):
|
||||
"""Calibrate the ePaper display to prevent burn-ins (ghosting)
|
||||
use this command to manually calibrate the display"""
|
||||
|
||||
self.Display.calibrate()
|
||||
|
||||
def _calibration_check(self):
|
||||
"""Calibration sheduler
|
||||
uses calibration hours from settings file to check if calibration is due"""
|
||||
now = arrow.now()
|
||||
print('hour:', now.hour, 'hours:', self._calibration_hours)
|
||||
print('state:', self._calibration_state)
|
||||
if now.hour in self._calibration_hours and self._calibration_state == False:
|
||||
self.calibrate()
|
||||
self._calibration_state = True
|
||||
else:
|
||||
self._calibration_state = False
|
||||
|
||||
|
||||
def _check_for_updates(self):
|
||||
"""Check if a new update is available for inkycal"""
|
||||
|
||||
raise NotImplementedError('Tha developer were too lazy to implement this..')
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _add_module(filepath_module, classname):
|
||||
"""Add a third party module to inkycal
|
||||
filepath_module = the full path of your module. The file should be in /modules!
|
||||
classname = the name of your class inside the module
|
||||
"""
|
||||
|
||||
# Path for modules
|
||||
_module_path = 'inkycal/modules/'
|
||||
|
||||
# Check if the filepath is a string
|
||||
if not isinstance(filepath_module, str):
|
||||
raise ValueError('filepath has to be a string!')
|
||||
|
||||
# Check if the classname is a string
|
||||
if not isinstance(classname, str):
|
||||
raise ValueError('classname has to be a string!')
|
||||
|
||||
# TODO:
|
||||
# Ensure only third-party modules are deleted as built-in modules
|
||||
# should not be deleted
|
||||
|
||||
# Check if module is inside the modules folder
|
||||
if not _module_path in filepath_module:
|
||||
raise Exception('Your module should be in', _module_path)
|
||||
|
||||
# Get the name of the third-party module file without extension (.py)
|
||||
filename = filepath_module.split('.py')[0].split('/')[-1]
|
||||
|
||||
# Check if filename or classname is in the current module init file
|
||||
with open('modules/__init__.py', mode ='r') as module_init:
|
||||
content = module_init.read().splitlines()
|
||||
|
||||
for line in content:
|
||||
if (filename or clasname) in line:
|
||||
raise Exception(
|
||||
'A module with this filename or classname already exists')
|
||||
|
||||
# Check if filename or classname is in the current inkycal init file
|
||||
with open('__init__.py', mode ='r') as inkycal_init:
|
||||
content = inkycal_init.read().splitlines()
|
||||
|
||||
for line in content:
|
||||
if (filename or clasname) in line:
|
||||
raise Exception(
|
||||
'A module with this filename or classname already exists')
|
||||
|
||||
# If all checks have passed, add the module in the module init file
|
||||
with open('modules/__init__.py', mode='a') as module_init:
|
||||
module_init.write('from .{} import {}'.format(filename, classname))
|
||||
|
||||
# If all checks have passed, add the module in the inkycal init file
|
||||
with open('__init__.py', mode ='a') as inkycal_init:
|
||||
inkycal_init.write('# Added by module adder \n')
|
||||
inkycal_init.write('import inkycal.modules.{}'.format(filename))
|
||||
|
||||
print('Your module {} has been added successfully! Hooray!'.format(
|
||||
classname))
|
||||
|
||||
@staticmethod
|
||||
def _remove_module(classname, remove_file = True):
|
||||
"""Removes a third-party module from inkycal
|
||||
Input the classname of the file you want to remove
|
||||
"""
|
||||
|
||||
# Check if filename or classname is in the current module init file
|
||||
with open('modules/__init__.py', mode ='r') as module_init:
|
||||
content = module_init.read().splitlines()
|
||||
|
||||
with open('modules/__init__.py', mode ='w') as module_init:
|
||||
for line in content:
|
||||
if not classname in line:
|
||||
module_init.write(line+'\n')
|
||||
else:
|
||||
filename = line.split(' ')[1].split('.')[1]
|
||||
|
||||
# Check if filename or classname is in the current inkycal init file
|
||||
with open('__init__.py', mode ='r') as inkycal_init:
|
||||
content = inkycal_init.read().splitlines()
|
||||
|
||||
with open('__init__.py', mode ='w') as inkycal_init:
|
||||
for line in content:
|
||||
if not filename in line:
|
||||
inkycal_init.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('modules/{}.py'.format(filename)) and remove_file == True:
|
||||
os.remove('modules/{}.py'.format(filename))
|
||||
|
||||
print('The module {} has been removed successfully'.format(classname))
|
||||
|
||||
|
@@ -16,35 +16,60 @@ filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
|
||||
class Agenda(inkycal_module):
|
||||
"""Agenda class
|
||||
Create agenda and show events from given icalendars
|
||||
"""
|
||||
|
||||
name = "Inkycal Agenda"
|
||||
|
||||
requires = {
|
||||
"ical_urls" : {
|
||||
"label":"iCalendar URL/s, separate multiple ones with a comma",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
optional = {
|
||||
"ical_files" : {
|
||||
"label":"iCalendar filepaths, separated with a comma",
|
||||
"default":[]
|
||||
},
|
||||
|
||||
"date_format":{
|
||||
"label":"Use an arrow-supported token for custom date formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. ddd D MMM",
|
||||
"default": "ddd D MMM",
|
||||
},
|
||||
|
||||
"time_format":{
|
||||
"label":"Use an arrow-supported token for custom time formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_agenda module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
# Module specific parameters
|
||||
required = ['week_starts_on', 'ical_urls']
|
||||
for param in required:
|
||||
|
||||
for param in self.equires:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# class name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# module specific parameters
|
||||
self.date_format = 'ddd D MMM'
|
||||
self.time_format = "HH:mm"
|
||||
self.date_format = self.config['date_format']
|
||||
self.time_format = self.config['time_format']
|
||||
self.language = self.config['language']
|
||||
self.timezone = get_system_tz()
|
||||
self.ical_urls = self.config['ical_urls']
|
||||
self.ical_files = []
|
||||
self.ical_files = self.config['ical_files']
|
||||
|
||||
self.timezone = get_system_tz()
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
print('{0} loaded'.format(filename))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
@@ -191,7 +216,6 @@ class Agenda(inkycal_module):
|
||||
|
||||
# If no events were found, write only dates and lines
|
||||
else:
|
||||
line_pos = [(0, int(line * line_height)) for line in range(max_lines)]
|
||||
cursor = 0
|
||||
for _ in agenda_events:
|
||||
title = _['title']
|
||||
@@ -206,9 +230,8 @@ class Agenda(inkycal_module):
|
||||
|
||||
logger.info('no events found')
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png')
|
||||
im_colour.save(images+self.name+'_colour.png')
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
@@ -19,42 +19,74 @@ class Calendar(inkycal_module):
|
||||
Create monthly calendar and show events from given icalendars
|
||||
"""
|
||||
|
||||
name = "Inkycal Calendar"
|
||||
|
||||
optional = {
|
||||
|
||||
"week_starts_on" : {
|
||||
"label":"When does your week start? (default=Monday)",
|
||||
"options": ["Monday", "Sunday"],
|
||||
"default": "Monday"
|
||||
},
|
||||
|
||||
"show_events" : {
|
||||
"label":"Show parsed events? (default = True)",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
"ical_urls" : {
|
||||
"label":"iCalendar URL/s, separate multiple ones with a comma",
|
||||
"default":[]
|
||||
},
|
||||
|
||||
"ical_files" : {
|
||||
"label":"iCalendar filepaths, separated with a comma",
|
||||
"default":[]
|
||||
},
|
||||
|
||||
"date_format":{
|
||||
"label":"Use an arrow-supported token for custom date formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
||||
"default": "D MMM",
|
||||
},
|
||||
|
||||
"time_format":{
|
||||
"label":"Use an arrow-supported token for custom time formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
"default": "HH:mm"
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_calendar module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
required = ['week_starts_on']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# module specific parameters
|
||||
self.num_font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size = self.fontsize)
|
||||
self.weekstart = self.config['week_starts_on']
|
||||
self.show_events = True
|
||||
self.date_format = 'D MMM'
|
||||
self.time_format = "HH:mm"
|
||||
self.show_events = self.config['show_events']
|
||||
self.date_format = self.config["date_format"]
|
||||
self.time_format = self.config['time_format']
|
||||
self.language = self.config['language']
|
||||
|
||||
self.timezone = get_system_tz()
|
||||
self.ical_urls = self.config['ical_urls']
|
||||
self.ical_files = []
|
||||
self.ical_files = self.config['ical_files']
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
print('{0} loaded'.format(filename))
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = int(self.width - (self.width * 2 * self.margin_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.margin_y))
|
||||
im_width = int(self.width - (2 * self.padding_x))
|
||||
im_height = int(self.height - (2 * self.padding_y))
|
||||
im_size = im_width, im_height
|
||||
|
||||
logger.info('Image size: {0}'.format(im_size))
|
||||
@@ -80,15 +112,7 @@ class Calendar(inkycal_module):
|
||||
im_width, calendar_height))
|
||||
|
||||
# Create grid and calculate icon sizes
|
||||
now = arrow.now(tz = self.timezone)
|
||||
monthstart = now.span('month')[0].weekday()
|
||||
monthdays = now.ceil('month').day
|
||||
|
||||
if monthstart > 4 and monthdays == 31:
|
||||
calendar_rows, calendar_cols = 7, 7
|
||||
else:
|
||||
calendar_rows, calendar_cols = 6, 7
|
||||
|
||||
calendar_rows, calendar_cols = 6, 7
|
||||
icon_width = im_width // calendar_cols
|
||||
icon_height = calendar_height // calendar_rows
|
||||
|
||||
@@ -106,6 +130,8 @@ class Calendar(inkycal_module):
|
||||
weekday_pos = [(grid_start_x + icon_width*_, month_name_height) for _ in
|
||||
range(calendar_cols)]
|
||||
|
||||
now = arrow.now(tz = self.timezone)
|
||||
|
||||
# Set weekstart of calendar to specified weekstart
|
||||
if self.weekstart == "Monday":
|
||||
cal.setfirstweekday(cal.MONDAY)
|
||||
@@ -283,9 +309,8 @@ class Calendar(inkycal_module):
|
||||
(im_width, self.font.getsize(symbol)[1]), symbol,
|
||||
font = self.font)
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png')
|
||||
im_colour.save(images+self.name+'_colour.png')
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
@@ -1,305 +1,32 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Image module for Inkycal Project
|
||||
Image module for inkycal Project
|
||||
Copyright by aceisace
|
||||
Development satge: Beta
|
||||
"""
|
||||
|
||||
from inkycal.modules.template import inkycal_module
|
||||
from inkycal.custom import *
|
||||
|
||||
from os import path
|
||||
from PIL import ImageOps
|
||||
import requests
|
||||
import numpy
|
||||
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.DEBUG)
|
||||
"""----------------------------------------------------------------"""
|
||||
#path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png'
|
||||
#path ='/home/pi/Inky-Calendar/images/canvas.png'
|
||||
path = inkycal_image_path
|
||||
path_body = inkycal_image_path_body
|
||||
mode = 'auto' # 'horizontal' # 'vertical' # 'auto'
|
||||
upside_down = False # Flip image by 180 deg (upside-down)
|
||||
alignment = 'center' # top_center, top_left, center_left, bottom_right etc.
|
||||
colours = 'bwr' # bwr # bwy # bw
|
||||
render = True # show image on E-Paper?
|
||||
"""----------------------------------------------------------------"""
|
||||
|
||||
class Inkyimage(inkycal_module):
|
||||
"""Image class
|
||||
display an image from a given path or URL
|
||||
"""
|
||||
_allowed_layout = ['fill', 'center', 'fit', 'auto']
|
||||
_allowed_rotation = [0, 90, 180, 270, 360, 'auto']
|
||||
_allowed_colours = ['bw', 'bwr', 'bwy']
|
||||
# First determine dimensions
|
||||
if mode == 'horizontal':
|
||||
display_width, display_height == display_height, display_width
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
required = ['path']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# module specific parameters
|
||||
self.image_path = self.config['path']
|
||||
|
||||
self.rotation = 0 #0 #90 # 180 # 270 # auto
|
||||
self.layout = 'fill' # centre # fit # auto
|
||||
self.colours = 'bw' #grab from settings file?
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
|
||||
# Validate image_path
|
||||
if not isinstance(self.image_path, str):
|
||||
print(
|
||||
'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"')
|
||||
|
||||
# Validate layout
|
||||
if not isinstance(self.layout, str) or (
|
||||
self.layout not in self._allowed_layout):
|
||||
print('layout has to be one of the following:', self._allowed_layout)
|
||||
|
||||
# Validate rotation angle
|
||||
if self.rotation not in self._allowed_rotation:
|
||||
print('rotation has to be one of the following:', self._allowed_rotation)
|
||||
|
||||
# Validate colours
|
||||
if not isinstance(self.colours, str) or (
|
||||
self.colours not in self._allowed_colours):
|
||||
print('colour has to be one of the following:', self._allowed_colours)
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = self.width
|
||||
im_height = self.height
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
# Try to open the image if it exists and is an image file
|
||||
try:
|
||||
if self.image_path.startswith('http'):
|
||||
logger.debug('identified url')
|
||||
self.image = Image.open(requests.get(self.image_path, stream=True).raw)
|
||||
else:
|
||||
logger.info('identified local path')
|
||||
self.image = Image.open(self.image_path)
|
||||
except FileNotFoundError:
|
||||
raise ('Your file could not be found. Please check the filepath')
|
||||
except OSError:
|
||||
raise ('Please check if the path points to an image file.')
|
||||
|
||||
logger.debug(('image-width:', self.image.width))
|
||||
logger.debug(('image-height:', self.image.height))
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size = im_size, color = 'white')
|
||||
im_colour = Image.new('RGB', size = im_size, color = 'white')
|
||||
|
||||
# do the required operations
|
||||
self._remove_alpha()
|
||||
self._to_layout()
|
||||
black, colour = self._map_colours()
|
||||
|
||||
# paste the imaeges on the canvas
|
||||
im_black.paste(black, (self.x, self.y))
|
||||
if colour != None:
|
||||
im_colour.paste(colour, (self.x, self.y))
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png', 'PNG')
|
||||
if colour != None:
|
||||
im_colour.save(images+self.name+'_colour.png', 'PNG')
|
||||
|
||||
def _rotate(self, angle=None):
|
||||
"""Rotate the image to a given angle
|
||||
angle must be one of :[0, 90, 180, 270, 360, 'auto']
|
||||
"""
|
||||
im = self.image
|
||||
if angle == None:
|
||||
angle = self.rotation
|
||||
|
||||
# Check if angle is supported
|
||||
if angle not in self._allowed_rotation:
|
||||
print('invalid angle provided, setting to fallback: 0 deg')
|
||||
angle = 0
|
||||
|
||||
# Autoflip the image if angle == 'auto'
|
||||
if angle == 'auto':
|
||||
if (im.width > self.height) and (im.width < self.height):
|
||||
print('display vertical, image horizontal -> flipping image')
|
||||
image = im.rotate(90, expand=True)
|
||||
if (im.width < self.height) and (im.width > self.height):
|
||||
print('display horizontal, image vertical -> flipping image')
|
||||
image = im.rotate(90, expand=True)
|
||||
# if not auto, flip to specified angle
|
||||
else:
|
||||
image = im.rotate(angle, expand = True)
|
||||
self.image = image
|
||||
|
||||
def _fit_width(self, width=None):
|
||||
"""Resize an image to desired width"""
|
||||
im = self.image
|
||||
if width == None: width = self.width
|
||||
|
||||
logger.debug(('resizing width from', im.width, 'to'))
|
||||
wpercent = (width/float(im.width))
|
||||
hsize = int((float(im.height)*float(wpercent)))
|
||||
image = im.resize((width, hsize), Image.ANTIALIAS)
|
||||
logger.debug(image.width)
|
||||
self.image = image
|
||||
|
||||
def _fit_height(self, height=None):
|
||||
"""Resize an image to desired height"""
|
||||
im = self.image
|
||||
if height == None: height = self.height
|
||||
|
||||
logger.debug(('resizing height from', im.height, 'to'))
|
||||
hpercent = (height / float(im.height))
|
||||
wsize = int(float(im.width) * float(hpercent))
|
||||
image = im.resize((wsize, height), Image.ANTIALIAS)
|
||||
logger.debug(image.height)
|
||||
self.image = image
|
||||
|
||||
def _to_layout(self, mode=None):
|
||||
"""Adjust the image to suit the layout
|
||||
mode can be center, fit or fill"""
|
||||
|
||||
im = self.image
|
||||
if mode == None: mode = self.layout
|
||||
|
||||
if mode not in self._allowed_layout:
|
||||
print('{} is not supported. Should be one of {}'.format(
|
||||
mode, self._allowed_layout))
|
||||
print('setting layout to fallback: centre')
|
||||
mode = 'center'
|
||||
|
||||
# If mode is center, just center the image
|
||||
if mode == 'center':
|
||||
pass
|
||||
|
||||
# if mode is fit, adjust height of the image while keeping ascept-ratio
|
||||
if mode == 'fit':
|
||||
self._fit_height()
|
||||
|
||||
# if mode is fill, enlargen or shrink the image to fit width
|
||||
if mode == 'fill':
|
||||
self._fit_width()
|
||||
|
||||
# in auto mode, flip image automatically and fit both height and width
|
||||
if mode == 'auto':
|
||||
|
||||
# Check if width is bigger than height and rotate by 90 deg if true
|
||||
if im.width > im.height:
|
||||
self._rotate(90)
|
||||
|
||||
# fit both height and width
|
||||
self._fit_height()
|
||||
self._fit_width()
|
||||
|
||||
if self.image.width > self.width:
|
||||
x = int( (self.image.width - self.width) / 2)
|
||||
else:
|
||||
x = int( (self.width - self.image.width) / 2)
|
||||
|
||||
if self.image.height > self.height:
|
||||
y = int( (self.image.height - self.height) / 2)
|
||||
else:
|
||||
y = int( (self.height - self.image.height) / 2)
|
||||
|
||||
self.x, self.y = x, y
|
||||
|
||||
def _remove_alpha(self):
|
||||
im = self.image
|
||||
|
||||
if len(im.getbands()) == 4:
|
||||
logger.debug('removing transparency')
|
||||
bg = Image.new('RGBA', (im.width, im.height), 'white')
|
||||
im = Image.alpha_composite(bg, im)
|
||||
self.image.paste(im, (0,0))
|
||||
|
||||
def _map_colours(self, colours = None):
|
||||
"""Map image colours to display-supported colours """
|
||||
im = self.image.convert('RGB')
|
||||
if colours == None: colours = self.colours
|
||||
|
||||
if colours not in self._allowed_colours:
|
||||
print('invalid colour: "{}", has to be one of: {}'.format(
|
||||
colours, self._allowed_colours))
|
||||
print('setting to fallback: bw')
|
||||
colours = 'bw'
|
||||
|
||||
if colours == 'bw':
|
||||
|
||||
# For black-white images, use monochrome dithering
|
||||
im_black = im.convert('1', dither=True)
|
||||
im_colour = None
|
||||
|
||||
elif colours == 'bwr':
|
||||
# For black-white-red images, create corresponding palette
|
||||
pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255]
|
||||
|
||||
elif colours == 'bwy':
|
||||
# For black-white-yellow images, create corresponding palette"""
|
||||
pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255]
|
||||
|
||||
# Map each pixel of the opened image to the Palette
|
||||
if colours == 'bwr' or colours == 'bwy':
|
||||
palette_im = Image.new('P', (3,1))
|
||||
palette_im.putpalette(pal * 64)
|
||||
quantized_im = im.quantize(palette=palette_im)
|
||||
quantized_im.convert('RGB')
|
||||
|
||||
# Create buffer for coloured pixels
|
||||
buffer1 = numpy.array(quantized_im.convert('RGB'))
|
||||
r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2]
|
||||
|
||||
# Create buffer for black pixels
|
||||
buffer2 = numpy.array(quantized_im.convert('RGB'))
|
||||
r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2]
|
||||
|
||||
if colours == 'bwr':
|
||||
# Create image for only red pixels
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black
|
||||
im_colour = Image.fromarray(buffer2)
|
||||
|
||||
# Create image for only black pixels
|
||||
buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255]
|
||||
im_black = Image.fromarray(buffer1)
|
||||
|
||||
if colours == 'bwy':
|
||||
# Create image for only yellow pixels
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black
|
||||
im_colour = Image.fromarray(buffer2)
|
||||
|
||||
# Create image for only black pixels
|
||||
buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255]
|
||||
im_black = Image.fromarray(buffer1)
|
||||
|
||||
return im_black, im_colour
|
||||
|
||||
@staticmethod
|
||||
def save(image):
|
||||
im = self.image
|
||||
im.save('/home/pi/Desktop/test.png', 'PNG')
|
||||
|
||||
@staticmethod
|
||||
def _show(image):
|
||||
"""Preview the 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')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
|
||||
## a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"})
|
||||
## a.generate_image()
|
||||
|
||||
|
||||
print('Done')
|
||||
|
305
inkycal/modules/inkycal_image2.py
Normal file
305
inkycal/modules/inkycal_image2.py
Normal file
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Image module for Inkycal Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
from inkycal.modules.template import inkycal_module
|
||||
from inkycal.custom import *
|
||||
|
||||
from PIL import ImageOps
|
||||
import requests
|
||||
import numpy
|
||||
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
class Inkyimage(inkycal_module):
|
||||
"""Image class
|
||||
display an image from a given path or URL
|
||||
"""
|
||||
|
||||
name = "Inykcal Image - show an image from a URL or local path"
|
||||
|
||||
requires = {
|
||||
'path': {
|
||||
"label":"Please enter the path of the image file (local or URL)",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
optional = {
|
||||
'rotation':{
|
||||
"label":"Specify the angle to rotate the image. Default is 0",
|
||||
"options": [0, 90, 180, 270, 360, "auto"],
|
||||
"default":0,
|
||||
},
|
||||
'layout':{
|
||||
"label":"How should the image be displayed on the display? Default is auto",
|
||||
"options": ['fill', 'center', 'fit', 'auto'],
|
||||
"default": "auto"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# required parameters
|
||||
for param in self.requires:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# optional parameters
|
||||
self.image_path = self.config['path']
|
||||
|
||||
self.rotation = self.config['rotation']
|
||||
self.layout = self.config['layout']
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
|
||||
# Validate image_path
|
||||
if not isinstance(self.image_path, str):
|
||||
print(
|
||||
'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"')
|
||||
|
||||
# Validate layout
|
||||
if not isinstance(self.layout, str):
|
||||
print('layout has to be a string')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = self.width
|
||||
im_height = self.height
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
# Try to open the image if it exists and is an image file
|
||||
try:
|
||||
if self.image_path.startswith('http'):
|
||||
logger.debug('identified url')
|
||||
self.image = Image.open(requests.get(self.image_path, stream=True).raw)
|
||||
else:
|
||||
logger.info('identified local path')
|
||||
self.image = Image.open(self.image_path)
|
||||
except FileNotFoundError:
|
||||
raise ('Your file could not be found. Please check the filepath')
|
||||
except OSError:
|
||||
raise ('Please check if the path points to an image file.')
|
||||
|
||||
logger.debug(('image-width:', self.image.width))
|
||||
logger.debug(('image-height:', self.image.height))
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size = im_size, color = 'white')
|
||||
im_colour = Image.new('RGB', size = im_size, color = 'white')
|
||||
|
||||
# do the required operations
|
||||
self._remove_alpha()
|
||||
self._to_layout()
|
||||
black, colour = self._map_colours()
|
||||
|
||||
# paste the images on the canvas
|
||||
im_black.paste(black, (self.x, self.y))
|
||||
im_colour.paste(colour, (self.x, self.y))
|
||||
|
||||
# Save images of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png', 'PNG')
|
||||
im_colour.save(images+self.name+'_colour.png', 'PNG')
|
||||
|
||||
def _rotate(self, angle=None):
|
||||
"""Rotate the image to a given angle
|
||||
angle must be one of :[0, 90, 180, 270, 360, 'auto']
|
||||
"""
|
||||
im = self.image
|
||||
if angle == None:
|
||||
angle = self.rotation
|
||||
|
||||
# Check if angle is supported
|
||||
if angle not in self._allowed_rotation:
|
||||
print('invalid angle provided, setting to fallback: 0 deg')
|
||||
angle = 0
|
||||
|
||||
# Autoflip the image if angle == 'auto'
|
||||
if angle == 'auto':
|
||||
if (im.width > self.height) and (im.width < self.height):
|
||||
print('display vertical, image horizontal -> flipping image')
|
||||
image = im.rotate(90, expand=True)
|
||||
if (im.width < self.height) and (im.width > self.height):
|
||||
print('display horizontal, image vertical -> flipping image')
|
||||
image = im.rotate(90, expand=True)
|
||||
# if not auto, flip to specified angle
|
||||
else:
|
||||
image = im.rotate(angle, expand = True)
|
||||
self.image = image
|
||||
|
||||
def _fit_width(self, width=None):
|
||||
"""Resize an image to desired width"""
|
||||
im = self.image
|
||||
if width == None: width = self.width
|
||||
|
||||
logger.debug(('resizing width from', im.width, 'to'))
|
||||
wpercent = (width/float(im.width))
|
||||
hsize = int((float(im.height)*float(wpercent)))
|
||||
image = im.resize((width, hsize), Image.ANTIALIAS)
|
||||
logger.debug(image.width)
|
||||
self.image = image
|
||||
|
||||
def _fit_height(self, height=None):
|
||||
"""Resize an image to desired height"""
|
||||
im = self.image
|
||||
if height == None: height = self.height
|
||||
|
||||
logger.debug(('resizing height from', im.height, 'to'))
|
||||
hpercent = (height / float(im.height))
|
||||
wsize = int(float(im.width) * float(hpercent))
|
||||
image = im.resize((wsize, height), Image.ANTIALIAS)
|
||||
logger.debug(image.height)
|
||||
self.image = image
|
||||
|
||||
def _to_layout(self, mode=None):
|
||||
"""Adjust the image to suit the layout
|
||||
mode can be center, fit or fill"""
|
||||
|
||||
im = self.image
|
||||
if mode == None: mode = self.layout
|
||||
|
||||
if mode not in self._allowed_layout:
|
||||
print('{} is not supported. Should be one of {}'.format(
|
||||
mode, self._allowed_layout))
|
||||
print('setting layout to fallback: centre')
|
||||
mode = 'center'
|
||||
|
||||
# If mode is center, just center the image
|
||||
if mode == 'center':
|
||||
pass
|
||||
|
||||
# if mode is fit, adjust height of the image while keeping ascept-ratio
|
||||
if mode == 'fit':
|
||||
self._fit_height()
|
||||
|
||||
# if mode is fill, enlargen or shrink the image to fit width
|
||||
if mode == 'fill':
|
||||
self._fit_width()
|
||||
|
||||
# in auto mode, flip image automatically and fit both height and width
|
||||
if mode == 'auto':
|
||||
|
||||
# Check if width is bigger than height and rotate by 90 deg if true
|
||||
if im.width > im.height:
|
||||
self._rotate(90)
|
||||
|
||||
# fit both height and width
|
||||
self._fit_height()
|
||||
self._fit_width()
|
||||
|
||||
if self.image.width > self.width:
|
||||
x = int( (self.image.width - self.width) / 2)
|
||||
else:
|
||||
x = int( (self.width - self.image.width) / 2)
|
||||
|
||||
if self.image.height > self.height:
|
||||
y = int( (self.image.height - self.height) / 2)
|
||||
else:
|
||||
y = int( (self.height - self.image.height) / 2)
|
||||
|
||||
self.x, self.y = x, y
|
||||
|
||||
def _remove_alpha(self):
|
||||
im = self.image
|
||||
|
||||
if len(im.getbands()) == 4:
|
||||
logger.debug('removing transparency')
|
||||
bg = Image.new('RGBA', (im.width, im.height), 'white')
|
||||
im = Image.alpha_composite(bg, im)
|
||||
self.image.paste(im, (0,0))
|
||||
|
||||
def _map_colours(self, colours = None):
|
||||
"""Map image colours to display-supported colours """
|
||||
im = self.image.convert('RGB')
|
||||
|
||||
if colours == 'bw':
|
||||
|
||||
# For black-white images, use monochrome dithering
|
||||
im_black = im.convert('1', dither=True)
|
||||
im_colour = None
|
||||
|
||||
elif colours == 'bwr':
|
||||
# For black-white-red images, create corresponding palette
|
||||
pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255]
|
||||
|
||||
elif colours == 'bwy':
|
||||
# For black-white-yellow images, create corresponding palette"""
|
||||
pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255]
|
||||
|
||||
# Map each pixel of the opened image to the Palette
|
||||
if colours == 'bwr' or colours == 'bwy':
|
||||
palette_im = Image.new('P', (3,1))
|
||||
palette_im.putpalette(pal * 64)
|
||||
quantized_im = im.quantize(palette=palette_im)
|
||||
quantized_im.convert('RGB')
|
||||
|
||||
# Create buffer for coloured pixels
|
||||
buffer1 = numpy.array(quantized_im.convert('RGB'))
|
||||
r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2]
|
||||
|
||||
# Create buffer for black pixels
|
||||
buffer2 = numpy.array(quantized_im.convert('RGB'))
|
||||
r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2]
|
||||
|
||||
if colours == 'bwr':
|
||||
# Create image for only red pixels
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black
|
||||
im_colour = Image.fromarray(buffer2)
|
||||
|
||||
# Create image for only black pixels
|
||||
buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255]
|
||||
im_black = Image.fromarray(buffer1)
|
||||
|
||||
if colours == 'bwy':
|
||||
# Create image for only yellow pixels
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black
|
||||
im_colour = Image.fromarray(buffer2)
|
||||
|
||||
# Create image for only black pixels
|
||||
buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255]
|
||||
im_black = Image.fromarray(buffer1)
|
||||
|
||||
return im_black, im_colour
|
||||
|
||||
@staticmethod
|
||||
def save(image, path):
|
||||
im = self.image
|
||||
im.save(path, 'PNG')
|
||||
|
||||
@staticmethod
|
||||
def _show(image):
|
||||
"""Preview the 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')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
|
||||
#a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"})
|
||||
#a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"})
|
||||
a = Inkyimage((480, 800), {'path': "/home/pi/Desktop/im/IMG_0475.JPG"})
|
||||
a.generate_image()
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
RSS module for Inky-Calendar Project
|
||||
RSS module for inkyCal Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
@@ -25,28 +25,48 @@ class RSS(inkycal_module):
|
||||
parses rss/atom feeds from given urls
|
||||
"""
|
||||
|
||||
name = "Inkycal RSS / Atom"
|
||||
|
||||
requires = {
|
||||
"rss_urls" : {
|
||||
"label":"Please enter ATOM or RSS feed URL/s, separated by a comma",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
optional = {
|
||||
|
||||
"shuffle_feeds": {
|
||||
"label": "Should the parsed RSS feeds be shuffled? (default=True)",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
required = ['rss_urls']
|
||||
for param in required:
|
||||
# Check if required parameters are available in config
|
||||
for param in self.requires:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
# parse required config
|
||||
self.rss_urls = self.config["rss_urls"].split(",")
|
||||
|
||||
# module specific parameters
|
||||
self.shuffle_feeds = True
|
||||
# parse optional config
|
||||
self.shuffle_feeds = self.config["shuffle_feeds"]
|
||||
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
print('{0} loaded'.format(filename))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
|
||||
if not isinstance(self.shuffle_feeds, bool):
|
||||
print('shuffle_feeds has to be a boolean: True/False')
|
||||
|
||||
@@ -55,8 +75,8 @@ class RSS(inkycal_module):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = int(self.width - (self.width * 2 * self.margin_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.margin_y))
|
||||
im_width = int(self.width - ( 2 * self.padding_x))
|
||||
im_height = int(self.height - (2 * self.padding_y))
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
@@ -70,7 +90,6 @@ class RSS(inkycal_module):
|
||||
else:
|
||||
raise Exception('Network could not be reached :/')
|
||||
|
||||
|
||||
# Set some parameters for formatting rss feeds
|
||||
line_spacing = 1
|
||||
line_height = self.font.getsize('hg')[1] + line_spacing
|
||||
@@ -86,7 +105,7 @@ class RSS(inkycal_module):
|
||||
|
||||
# Create list containing all rss-feeds from all rss-feed urls
|
||||
parsed_feeds = []
|
||||
for feeds in self.config['rss_urls']:
|
||||
for feeds in self.rss_urls:
|
||||
text = feedparser.parse(feeds)
|
||||
for posts in text.entries:
|
||||
parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary))
|
||||
@@ -127,8 +146,8 @@ class RSS(inkycal_module):
|
||||
del filtered_feeds, parsed_feeds, wrapped, counter, text
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png', 'PNG')
|
||||
im_colour.save(images+self.name+'_colour.png', 'PNG')
|
||||
return im_black, im_colour
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
print(RSS.get_config())
|
||||
|
41
inkycal/modules/inkycal_server.py
Normal file
41
inkycal/modules/inkycal_server.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Image Server module for Inkycal project
|
||||
For use with Robert Sierre's inkycal web-service
|
||||
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
from os import path
|
||||
from PIL import ImageOps
|
||||
import requests
|
||||
import numpy
|
||||
|
||||
"""----------------------------------------------------------------"""
|
||||
#path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png'
|
||||
#path ='/home/pi/Inky-Calendar/images/canvas.png'
|
||||
path = inkycal_image_path
|
||||
path_body = inkycal_image_path_body
|
||||
mode = 'auto' # 'horizontal' # 'vertical' # 'auto'
|
||||
upside_down = False # Flip image by 180 deg (upside-down)
|
||||
alignment = 'center' # top_center, top_left, center_left, bottom_right etc.
|
||||
colours = 'bwr' # bwr # bwy # bw
|
||||
render = True # show image on E-Paper?
|
||||
"""----------------------------------------------------------------"""
|
||||
|
||||
|
||||
path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height))
|
||||
print(path)
|
||||
|
||||
try:
|
||||
# POST request, passing path_body in the body
|
||||
im = Image.open(requests.post(path, json=path_body, stream=True).raw)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise Exception('Your file could not be found. Please check the path to your file.')
|
||||
|
||||
except OSError:
|
||||
raise Exception('Please check if the path points to an image file.')
|
||||
|
||||
|
@@ -20,14 +20,157 @@ logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
|
||||
api = todoist.TodoistAPI('your api key')
|
||||
api.sync()
|
||||
class Todoist(inkycal_module):
|
||||
"""Todoist api class
|
||||
parses todo's from api-key
|
||||
"""
|
||||
|
||||
# Print name of author
|
||||
print(api.state['user']['full_name']+'\n')
|
||||
name = "Inkycal Todoist"
|
||||
|
||||
requires = {
|
||||
'api_key': {
|
||||
"label":"Please enter your Todoist API-key",
|
||||
},
|
||||
}
|
||||
|
||||
optional = {
|
||||
'project_filter': {
|
||||
"label":"Show Todos only from following project (separated by a comma). Leave empty to show "+
|
||||
"todos from all projects",
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
for param in self.requires:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
|
||||
tasks = (task.data for task in api.state['items'])
|
||||
# module specific parameters
|
||||
self.api_key = self.config['api_key']
|
||||
self.project_filter = self.config['project_filter']# only show todos from these projects
|
||||
|
||||
for _ in tasks:
|
||||
print('task: {} is {}'.format(_['content'], 'done' if _['checked'] == 1 else 'not done'))
|
||||
self._api = todoist.TodoistAPI(self.config['api_key'])
|
||||
self._api.sync()
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
if not isinstance(self.api_key, str):
|
||||
print('api_key has to be a string: "Yourtopsecretkey123" ')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = int(self.width - (2 * self.padding_x))
|
||||
im_height = int(self.height - (2 * self.padding_y))
|
||||
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
|
||||
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() == True:
|
||||
logger.info('Connection test passed')
|
||||
else:
|
||||
raise Exception('Network could not be reached :/')
|
||||
|
||||
# Set some parameters for formatting todos
|
||||
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))
|
||||
|
||||
# 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)]
|
||||
|
||||
#------------------------------------------------------------------------##
|
||||
# Get all projects by name and id
|
||||
all_projects = {project['name']: project['id']
|
||||
for project in self._api.projects.all()}
|
||||
|
||||
# Check if project from filter could be found
|
||||
if self.project_filter:
|
||||
for project in self.project_filter:
|
||||
if project not in all_projects:
|
||||
print('Could not find a project named {}'.format(project))
|
||||
self.project_filter.remove(project)
|
||||
|
||||
# function for extracting project names from tasks
|
||||
get_project_name = lambda task: (self._api.projects.get_data(
|
||||
task['project_id'])['project']['name'])
|
||||
|
||||
# If the filter is empty, parse all tasks which are not yet done
|
||||
if self.project_filter:
|
||||
tasks = (task.data for task in self._api.state['items']
|
||||
if (task['checked'] == 0) and
|
||||
(get_project_name(task) in self.project_filter))
|
||||
|
||||
# If filter is not empty, parse undone tasks in only those projects
|
||||
else:
|
||||
tasks = (task.data for task in self._api.state['items'] if
|
||||
(task['checked'] == 0))
|
||||
|
||||
# Simplify the tasks for faster processing
|
||||
simplified = [{'name':task['content'],
|
||||
'due':task['due'],
|
||||
'priority':task['priority'],
|
||||
'project_id':task['project_id']}
|
||||
for task in tasks]
|
||||
|
||||
# Group tasks by project name
|
||||
grouped = {}
|
||||
|
||||
if self.project_filter:
|
||||
for project in self.project_filter:
|
||||
project_id = all_projects[project]
|
||||
grouped[ project ] = [
|
||||
task for task in simplified if task['project_id'] == project_id]
|
||||
else:
|
||||
for project in all_projects:
|
||||
project_id = all_projects[project]
|
||||
grouped[ project ] = [
|
||||
task for task in simplified if task['project_id'] == project_id]
|
||||
|
||||
# Print tasks sorted by groups
|
||||
for project, tasks in grouped.items():
|
||||
print('*', project)
|
||||
for task in tasks:
|
||||
print('• {} {}'.format(
|
||||
task['due']['string'] if task['due'] != None else '', task['name']))
|
||||
|
||||
|
||||
## # Write rss-feeds on image
|
||||
## for _ in range(len(filtered_feeds)):
|
||||
## write(im_black, line_positions[_], (line_width, line_height),
|
||||
## filtered_feeds[_], font = self.font, alignment= 'left')
|
||||
|
||||
|
||||
|
||||
# Cleanup ---------------------------
|
||||
# del grouped, parsed_feeds, wrapped, counter, text
|
||||
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
config = {'api_key':'4e166367dcafdd60e6a9f4cbed598d578bf2c359'}
|
||||
size = (480, 100)
|
||||
a = Todoist(size, config)
|
||||
b,c = a.generate_image()
|
||||
|
@@ -26,6 +26,60 @@ class Weather(inkycal_module):
|
||||
"""Weather class
|
||||
parses weather details from openweathermap
|
||||
"""
|
||||
#TODO: automatic setup of pyowm by location id if location is numeric
|
||||
|
||||
name = "Inkycal Weather (openweathermap)"
|
||||
|
||||
requires = {
|
||||
|
||||
"api_key" : {
|
||||
"label":"Please enter openweathermap api-key. You can create one for free on openweathermap",
|
||||
},
|
||||
|
||||
"location": {
|
||||
"label":"Please enter your location in the following format: City, Country-Code"
|
||||
}
|
||||
}
|
||||
|
||||
optional = {
|
||||
|
||||
"round_temperature": {
|
||||
"label":"Round temperature to the nearest degree?",
|
||||
"options": [True, False],
|
||||
"default" : True
|
||||
},
|
||||
|
||||
"round_windspeed": {
|
||||
"label":"Round windspeed?",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
"forecast_interval": {
|
||||
"label":"Please select the forecast interval",
|
||||
"options": ["daily", "hourly"],
|
||||
"default": "daily"
|
||||
},
|
||||
|
||||
"units": {
|
||||
"label": "Which units should be used?",
|
||||
"options": ["metric", "imperial"],
|
||||
"default": "metric"
|
||||
},
|
||||
|
||||
"hour_format": {
|
||||
"label": "Which hour format do you prefer?",
|
||||
"options": [12, 24],
|
||||
"default": 24
|
||||
},
|
||||
|
||||
"use_beaufort": {
|
||||
"label": "Use beaufort scale for windspeed?",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_weather module"""
|
||||
@@ -33,35 +87,36 @@ class Weather(inkycal_module):
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
required = ['api_key','location']
|
||||
for param in required:
|
||||
for param in self.requires:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
# required parameters
|
||||
self.location = self.config['location']
|
||||
self.api_key = self.config['api_key']
|
||||
|
||||
# module specific parameters
|
||||
self.owm = pyowm.OWM(self.config['api_key'])
|
||||
# optional parameters
|
||||
self.round_temperature = self.config['round_temperature']
|
||||
self.round_windspeed = self.config['round_windspeed']
|
||||
self.forecast_interval = self.config['forecast_interval']
|
||||
self.units = self.config['units']
|
||||
self.hour_format = self.config['hours']
|
||||
self.hour_format = self.config['hour_format']
|
||||
self.use_beaufort = self.config['use_beaufort']
|
||||
self.timezone = get_system_tz()
|
||||
self.round_temperature = True
|
||||
self.round_windspeed = True
|
||||
self.use_beaufort = True
|
||||
self.forecast_interval = 'daily' # daily # hourly
|
||||
self.locale = sys_locale()[0]
|
||||
self.weatherfont = ImageFont.truetype(fonts['weathericons-regular-webfont'],
|
||||
size = self.fontsize)
|
||||
|
||||
#self.owm = pyowm.OWM(self.config['api_key'])
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
print('{0} loaded'.format(filename))
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = int(self.width - (self.width * 2 * self.margin_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.margin_y))
|
||||
im_width = int(self.width - (2 * self.padding_x))
|
||||
im_height = int(self.height - (2 * self.padding_y))
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
@@ -422,9 +477,8 @@ class Weather(inkycal_module):
|
||||
draw_border(im_black, (col6, row1), (col_width, im_height))
|
||||
draw_border(im_black, (col7, row1), (col_width, im_height))
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png', "PNG")
|
||||
im_colour.save(images+self.name+'_colour.png', "PNG")
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
@@ -10,14 +10,16 @@ class inkycal_module(metaclass=abc.ABCMeta):
|
||||
callable(subclass.generate_image) or
|
||||
NotImplemented)
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
def __init__(self, section_config):
|
||||
# Initializes base module
|
||||
# sets properties shared amongst all sections
|
||||
self.config = section_config
|
||||
self.width, self.height = section_size
|
||||
self.fontsize = 12
|
||||
self.margin_x = 0.02
|
||||
self.margin_y = 0.05
|
||||
self.width, self.height = section_config['size']
|
||||
|
||||
self.padding_left = self.padding_right = self.config["padding_x"]
|
||||
self.padding_top = self.padding_bottom = self.config["padding_y"]
|
||||
|
||||
self.fontsize = self.config["fontsize"]
|
||||
self.font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size = self.fontsize)
|
||||
|
||||
@@ -56,3 +58,33 @@ class inkycal_module(metaclass=abc.ABCMeta):
|
||||
# Generate image for this module with specified parameters
|
||||
raise NotImplementedError(
|
||||
'The developers were too lazy to implement this function')
|
||||
|
||||
@classmethod
|
||||
def get_config(cls):
|
||||
# Get the config of this module for the web-ui
|
||||
# Do not change
|
||||
try:
|
||||
|
||||
if hasattr(cls, 'requires'):
|
||||
for each in cls.requires:
|
||||
if not "label" in cls.requires[each]:
|
||||
raise Exception("no label found for {}".format(each))
|
||||
|
||||
if hasattr(cls, 'optional'):
|
||||
for each in cls.optional:
|
||||
if not "label" in cls.optional[each]:
|
||||
raise Exception("no label found for {}".format(each))
|
||||
|
||||
conf = {
|
||||
"name": cls.__name__,
|
||||
"name_str": cls.name,
|
||||
"requires": cls.requires if hasattr(cls, 'requires') else {},
|
||||
"optional": cls.optional if hasattr(cls, 'optional') else {},
|
||||
}
|
||||
return conf
|
||||
except:
|
||||
raise Exception(
|
||||
'Ohoh, something went wrong while trying to get the config of this module')
|
||||
|
||||
|
||||
|
||||
|
@@ -60,9 +60,21 @@ class Simple(inkycal_module):
|
||||
Explain what this module does...
|
||||
"""
|
||||
|
||||
# name is the name that will be shown on the web-ui
|
||||
# may be same or different to the class name (Do not remove this)
|
||||
name = "My own module"
|
||||
|
||||
# create a dictionary that specifies what your module absolutely needs
|
||||
# to run correctly
|
||||
# Use the following format -> "key" : "info about this key for web-ui"
|
||||
# You can add as many required entries as you like
|
||||
requires = {
|
||||
"module_parameter" : "Short info about this parameter, shown on the web-ui",
|
||||
}
|
||||
|
||||
# Initialise the class (do not remove)
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
"""Initialize your module module"""
|
||||
|
||||
# Initialise this module via the inkycal_module template (required)
|
||||
super().__init__(section_size, section_config)
|
||||
|
474
inkycal/old.py
Normal file
474
inkycal/old.py
Normal file
@@ -0,0 +1,474 @@
|
||||
from inkycal import Settings, Layout
|
||||
from inkycal.custom import *
|
||||
|
||||
#from os.path import exists
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
import arrow
|
||||
import time
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print('Pillow is not installed! Please install with:')
|
||||
print('pip3 install Pillow')
|
||||
|
||||
try:
|
||||
import numpy
|
||||
except ImportError:
|
||||
print('numpy is not installed! Please install with:')
|
||||
print('pip3 install numpy')
|
||||
|
||||
logger = logging.getLogger('inkycal')
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
class Inkycal:
|
||||
"""Inkycal main class"""
|
||||
|
||||
def __init__(self, settings_path, render=True):
|
||||
"""initialise class
|
||||
settings_path = str -> location/folder of settings file
|
||||
render = bool -> show something on the ePaper?
|
||||
"""
|
||||
self._release = '2.0.0beta'
|
||||
|
||||
# Check if render is boolean
|
||||
if not isinstance(render, bool):
|
||||
raise Exception('render must be True or False, not "{}"'.format(render))
|
||||
self.render = render
|
||||
|
||||
# Init settings class
|
||||
self.Settings = Settings(settings_path)
|
||||
|
||||
# Check if display support colour
|
||||
self.supports_colour = self.Settings.Layout.supports_colour
|
||||
|
||||
# Option to flip image upside down
|
||||
if self.Settings.display_orientation == 'normal':
|
||||
self.upside_down = False
|
||||
|
||||
elif self.Settings.display_orientation == 'upside_down':
|
||||
self.upside_down = True
|
||||
|
||||
# Option to use epaper image optimisation
|
||||
self.optimize = True
|
||||
|
||||
# Load drivers if image should be rendered
|
||||
if self.render == True:
|
||||
|
||||
# Get model and check if colour can be rendered
|
||||
model= self.Settings.model
|
||||
|
||||
# Init Display class
|
||||
from inkycal.display import Display
|
||||
self.Display = Display(model)
|
||||
|
||||
# get calibration hours
|
||||
self._calibration_hours = self.Settings.calibration_hours
|
||||
|
||||
# set a check for calibration
|
||||
self._calibration_state = False
|
||||
|
||||
# load+validate settings file. Import and setup specified modules
|
||||
self.active_modules = self.Settings.active_modules()
|
||||
for module in self.active_modules:
|
||||
try:
|
||||
loader = 'from inkycal.modules import {0}'.format(module)
|
||||
module_data = self.Settings.get_config(module)
|
||||
size, conf = module_data['size'], module_data['config']
|
||||
setup = 'self.{} = {}(size, conf)'.format(module, module)
|
||||
exec(loader)
|
||||
exec(setup)
|
||||
logger.debug(('{}: size: {}, config: {}'.format(module, size, conf)))
|
||||
|
||||
# If a module was not found, print an error message
|
||||
except ImportError:
|
||||
print(
|
||||
'Could not find module: "{}". Please try to import manually.'.format(
|
||||
module))
|
||||
|
||||
# Give an OK message
|
||||
print('loaded inkycal')
|
||||
|
||||
def countdown(self, interval_mins=None):
|
||||
"""Returns the remaining time in seconds until next display update"""
|
||||
|
||||
# Validate update interval
|
||||
allowed_intervals = [10, 15, 20, 30, 60]
|
||||
|
||||
# Check if empty, if empty, use value from settings file
|
||||
if interval_mins == None:
|
||||
interval_mins = self.Settings.update_interval
|
||||
|
||||
# Check if integer
|
||||
if not isinstance(interval_mins, int):
|
||||
raise Exception('Update interval must be an integer -> 60')
|
||||
|
||||
# Check if value is supported
|
||||
if interval_mins not in allowed_intervals:
|
||||
raise Exception('Update interval is {}, but should be one of: {}'.format(
|
||||
interval_mins, allowed_intervals))
|
||||
|
||||
# Find out at which minutes the update should happen
|
||||
now = arrow.now()
|
||||
update_timings = [(60 - int(interval_mins)*updates) for updates in
|
||||
range(60//int(interval_mins))][::-1]
|
||||
|
||||
# Calculate time in mins until next update
|
||||
minutes = [_ for _ in update_timings if _>= now.minute][0] - now.minute
|
||||
|
||||
# Print the remaining time in mins until next update
|
||||
print('{0} Minutes left until next refresh'.format(minutes))
|
||||
|
||||
# Calculate time in seconds until next update
|
||||
remaining_time = minutes*60 + (60 - now.second)
|
||||
|
||||
# Return seconds until next update
|
||||
return remaining_time
|
||||
|
||||
def test(self):
|
||||
"""Inkycal test run.
|
||||
Generates images for each module, one by one and prints OK if no
|
||||
problems were found."""
|
||||
print('You are running inkycal v{}'.format(self._release))
|
||||
|
||||
|
||||
print('Running inkycal test-run for {} ePaper'.format(
|
||||
self.Settings.model))
|
||||
|
||||
if self.upside_down == True:
|
||||
print('upside-down mode active')
|
||||
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
print('generating image for {} module...'.format(module), end = '')
|
||||
try:
|
||||
exec(generate_im)
|
||||
print('OK!')
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
print(traceback.format_exc())
|
||||
|
||||
def run(self):
|
||||
"""Runs the main inykcal program nonstop (cannot be stopped anymore!)
|
||||
Will show something on the display if render was set to True"""
|
||||
|
||||
# TODO: printing traceback on display (or at least a smaller message?)
|
||||
# Calibration
|
||||
|
||||
# Get the time of initial run
|
||||
runtime = arrow.now()
|
||||
|
||||
# Function to flip images upside down
|
||||
upside_down = lambda image: image.rotate(180, expand=True)
|
||||
|
||||
# Count the number of times without any errors
|
||||
counter = 1
|
||||
|
||||
# Calculate the max. fontsize for info-section
|
||||
if self.Settings.info_section == True:
|
||||
info_section_height = round(self.Settings.Layout.display_height* (1/95) )
|
||||
self.font = auto_fontsize(ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed']), info_section_height)
|
||||
|
||||
while True:
|
||||
print('Generating images for all modules...')
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
try:
|
||||
exec(generate_im)
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
message = traceback.format_exc()
|
||||
print(message)
|
||||
counter = 0
|
||||
print('OK')
|
||||
|
||||
# Assemble image from each module
|
||||
self._assemble()
|
||||
|
||||
# Check if image should be rendered
|
||||
if self.render == True:
|
||||
Display = self.Display
|
||||
|
||||
self._calibration_check()
|
||||
|
||||
if self.supports_colour == True:
|
||||
im_black = Image.open(images+'canvas.png')
|
||||
im_colour = Image.open(images+'canvas_colour.png')
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.upside_down == True:
|
||||
im_black = upside_down(im_black)
|
||||
im_colour = upside_down(im_colour)
|
||||
|
||||
# render the image on the display
|
||||
Display.render(im_black, im_colour)
|
||||
|
||||
# Part for black-white ePapers
|
||||
elif self.supports_colour == False:
|
||||
|
||||
im_black = self._merge_bands()
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.upside_down == True:
|
||||
im_black = upside_down(im_black)
|
||||
|
||||
Display.render(im_black)
|
||||
|
||||
print('\ninkycal has been running without any errors for', end = ' ')
|
||||
print('{} display updates'.format(counter))
|
||||
print('Programm started {}'.format(runtime.humanize()))
|
||||
|
||||
counter += 1
|
||||
|
||||
sleep_time = self.countdown()
|
||||
time.sleep(sleep_time)
|
||||
|
||||
def _merge_bands(self):
|
||||
"""Merges black and coloured bands for black-white ePapers
|
||||
returns the merged image
|
||||
"""
|
||||
|
||||
im_path = images
|
||||
|
||||
im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png'
|
||||
|
||||
# If there is an image for black and colour, merge them
|
||||
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
||||
|
||||
im1 = Image.open(im1_path).convert('RGBA')
|
||||
im2 = Image.open(im2_path).convert('RGBA')
|
||||
|
||||
def clear_white(img):
|
||||
"""Replace all white pixels from image with transparent pixels
|
||||
"""
|
||||
x = numpy.asarray(img.convert('RGBA')).copy()
|
||||
x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8)
|
||||
return Image.fromarray(x)
|
||||
|
||||
im2 = clear_white(im2)
|
||||
im1.paste(im2, (0,0), im2)
|
||||
|
||||
# If there is no image for the coloured-band, return the bw-image
|
||||
elif os.path.exists(im1_path) and not os.path.exists(im2_path):
|
||||
im1 = Image.open(im1_name).convert('RGBA')
|
||||
|
||||
return im1
|
||||
|
||||
|
||||
def _assemble(self):
|
||||
"""Assmebles all sub-images to a single image"""
|
||||
|
||||
# Create an empty canvas with the size of the display
|
||||
width, height = self.Settings.Layout.display_size
|
||||
|
||||
if self.Settings.info_section == True:
|
||||
height = round(height * ((1/95)*100) )
|
||||
|
||||
im_black = Image.new('RGB', (width, height), color = 'white')
|
||||
im_colour = Image.new('RGB', (width ,height), color = 'white')
|
||||
|
||||
# Set cursor for y-axis
|
||||
im1_cursor = 0
|
||||
im2_cursor = 0
|
||||
|
||||
for module in self.active_modules:
|
||||
|
||||
im1_path = images+module+'.png'
|
||||
im2_path = images+module+'_colour.png'
|
||||
|
||||
# Check if there is an image for the black band
|
||||
if os.path.exists(im1_path):
|
||||
|
||||
# Get actual size of image
|
||||
im1 = Image.open(im1_path).convert('RGBA')
|
||||
im1_size = im1.size
|
||||
|
||||
# Get the size of the section
|
||||
section_size = self.Settings.get_config(module)['size']
|
||||
# Calculate coordinates to center the image
|
||||
x = int( (section_size[0] - im1_size[0]) /2)
|
||||
|
||||
# If this is the first module, use the y-offset
|
||||
if im1_cursor == 0:
|
||||
y = int( (section_size[1]-im1_size[1]) /2)
|
||||
else:
|
||||
y = im1_cursor + int( (section_size[1]-im1_size[1]) /2)
|
||||
|
||||
# center the image in the section space
|
||||
im_black.paste(im1, (x,y), im1)
|
||||
|
||||
# Shift the y-axis cursor at the beginning of next section
|
||||
im1_cursor += section_size[1]
|
||||
|
||||
# Check if there is an image for the coloured band
|
||||
if os.path.exists(im2_path):
|
||||
|
||||
# Get actual size of image
|
||||
im2 = Image.open(im2_path).convert('RGBA')
|
||||
im2_size = im2.size
|
||||
|
||||
# Get the size of the section
|
||||
section_size = self.Settings.get_config(module)['size']
|
||||
|
||||
# Calculate coordinates to center the image
|
||||
x = int( (section_size[0]-im2_size[0]) /2)
|
||||
|
||||
# If this is the first module, use the y-offset
|
||||
if im2_cursor == 0:
|
||||
y = int( (section_size[1]-im2_size[1]) /2)
|
||||
else:
|
||||
y = im2_cursor + int( (section_size[1]-im2_size[1]) /2)
|
||||
|
||||
# center the image in the section space
|
||||
im_colour.paste(im2, (x,y), im2)
|
||||
|
||||
# Shift the y-axis cursor at the beginning of next section
|
||||
im2_cursor += section_size[1]
|
||||
|
||||
# Show an info section if specified by the settings file
|
||||
now = arrow.now()
|
||||
stamp = 'last update: {}'.format(now.format('D MMM @ HH:mm', locale =
|
||||
self.Settings.language))
|
||||
if self.Settings.info_section == True:
|
||||
write(im_black, (0, im1_cursor), (width, height-im1_cursor),
|
||||
stamp, font = self.font)
|
||||
|
||||
|
||||
# optimize the image by mapping colours to pure black and white
|
||||
if self.optimize == True:
|
||||
self._optimize_im(im_black).save(images+'canvas.png', 'PNG')
|
||||
self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG')
|
||||
else:
|
||||
im_black.save(images+'canvas.png', 'PNG')
|
||||
im_colour.save(images+'canvas_colour.png', 'PNG')
|
||||
|
||||
def _optimize_im(self, image, threshold=220):
|
||||
"""Optimize the image for rendering on ePaper displays"""
|
||||
|
||||
buffer = numpy.array(image.convert('RGB'))
|
||||
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
||||
# grey->black
|
||||
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0]
|
||||
image = Image.fromarray(buffer)
|
||||
return image
|
||||
|
||||
def calibrate(self):
|
||||
"""Calibrate the ePaper display to prevent burn-ins (ghosting)
|
||||
use this command to manually calibrate the display"""
|
||||
|
||||
self.Display.calibrate()
|
||||
|
||||
def _calibration_check(self):
|
||||
"""Calibration sheduler
|
||||
uses calibration hours from settings file to check if calibration is due"""
|
||||
now = arrow.now()
|
||||
print('hour:', now.hour, 'hours:', self._calibration_hours)
|
||||
print('state:', self._calibration_state)
|
||||
if now.hour in self._calibration_hours and self._calibration_state == False:
|
||||
self.calibrate()
|
||||
self._calibration_state = True
|
||||
else:
|
||||
self._calibration_state = False
|
||||
|
||||
|
||||
def _check_for_updates(self):
|
||||
"""Check if a new update is available for inkycal"""
|
||||
|
||||
raise NotImplementedError('Tha developer were too lazy to implement this..')
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _add_module(filepath_module, classname):
|
||||
"""Add a third party module to inkycal
|
||||
filepath_module = the full path of your module. The file should be in /modules!
|
||||
classname = the name of your class inside the module
|
||||
"""
|
||||
|
||||
# Path for modules
|
||||
_module_path = 'inkycal/modules/'
|
||||
|
||||
# Check if the filepath is a string
|
||||
if not isinstance(filepath_module, str):
|
||||
raise ValueError('filepath has to be a string!')
|
||||
|
||||
# Check if the classname is a string
|
||||
if not isinstance(classname, str):
|
||||
raise ValueError('classname has to be a string!')
|
||||
|
||||
# TODO:
|
||||
# Ensure only third-party modules are deleted as built-in modules
|
||||
# should not be deleted
|
||||
|
||||
# Check if module is inside the modules folder
|
||||
if not _module_path in filepath_module:
|
||||
raise Exception('Your module should be in', _module_path)
|
||||
|
||||
# Get the name of the third-party module file without extension (.py)
|
||||
filename = filepath_module.split('.py')[0].split('/')[-1]
|
||||
|
||||
# Check if filename or classname is in the current module init file
|
||||
with open('modules/__init__.py', mode ='r') as module_init:
|
||||
content = module_init.read().splitlines()
|
||||
|
||||
for line in content:
|
||||
if (filename or clasname) in line:
|
||||
raise Exception(
|
||||
'A module with this filename or classname already exists')
|
||||
|
||||
# Check if filename or classname is in the current inkycal init file
|
||||
with open('__init__.py', mode ='r') as inkycal_init:
|
||||
content = inkycal_init.read().splitlines()
|
||||
|
||||
for line in content:
|
||||
if (filename or clasname) in line:
|
||||
raise Exception(
|
||||
'A module with this filename or classname already exists')
|
||||
|
||||
# If all checks have passed, add the module in the module init file
|
||||
with open('modules/__init__.py', mode='a') as module_init:
|
||||
module_init.write('from .{} import {}'.format(filename, classname))
|
||||
|
||||
# If all checks have passed, add the module in the inkycal init file
|
||||
with open('__init__.py', mode ='a') as inkycal_init:
|
||||
inkycal_init.write('# Added by module adder \n')
|
||||
inkycal_init.write('import inkycal.modules.{}'.format(filename))
|
||||
|
||||
print('Your module {} has been added successfully! Hooray!'.format(
|
||||
classname))
|
||||
|
||||
@staticmethod
|
||||
def _remove_module(classname, remove_file = True):
|
||||
"""Removes a third-party module from inkycal
|
||||
Input the classname of the file you want to remove
|
||||
"""
|
||||
|
||||
# Check if filename or classname is in the current module init file
|
||||
with open('modules/__init__.py', mode ='r') as module_init:
|
||||
content = module_init.read().splitlines()
|
||||
|
||||
with open('modules/__init__.py', mode ='w') as module_init:
|
||||
for line in content:
|
||||
if not classname in line:
|
||||
module_init.write(line+'\n')
|
||||
else:
|
||||
filename = line.split(' ')[1].split('.')[1]
|
||||
|
||||
# Check if filename or classname is in the current inkycal init file
|
||||
with open('__init__.py', mode ='r') as inkycal_init:
|
||||
content = inkycal_init.read().splitlines()
|
||||
|
||||
with open('__init__.py', mode ='w') as inkycal_init:
|
||||
for line in content:
|
||||
if not filename in line:
|
||||
inkycal_init.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('modules/{}.py'.format(filename)) and remove_file == True:
|
||||
os.remove('modules/{}.py'.format(filename))
|
||||
|
||||
print('The module {} has been removed successfully'.format(classname))
|
||||
|
Reference in New Issue
Block a user