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:
Ace
2020-11-09 17:51:15 +01:00
parent b6c2534644
commit 29788f0313
33 changed files with 2587 additions and 1765 deletions

View File

@@ -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))

View File

@@ -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))

View File

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

View 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()

View File

@@ -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())

View 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.')

View 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()

View File

@@ -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))

View File

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

View File

@@ -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)