Improved formatting

This commit is contained in:
aceisace
2022-04-02 01:30:17 +02:00
parent 9bf4385b6b
commit 5b032d6231
42 changed files with 3644 additions and 3661 deletions

View File

@@ -1,4 +1,4 @@
# Display class (for dirving E-Paper displays)
# Display class (for driving E-Paper displays)
from inkycal.display import Display
# Default modules

View File

@@ -16,7 +16,7 @@ logs.setLevel(level=logging.INFO)
# Get the path to the Inkycal folder
top_level = os.path.dirname(
os.path.abspath(os.path.dirname(__file__))).split('/inkycal')[0]
os.path.abspath(os.path.dirname(__file__))).split('/inkycal')[0]
# Get path of 'fonts' and 'images' folders within Inkycal folder
fonts_location = top_level + '/fonts/'
@@ -25,290 +25,290 @@ images = top_level + '/images/'
# Get available fonts within fonts folder
fonts = {}
for path,dirs,files in os.walk(fonts_location):
for filename in files:
if filename.endswith('.otf'):
name = filename.split('.otf')[0]
fonts[name] = os.path.join(path, filename)
for path, dirs, files in os.walk(fonts_location):
for filename in files:
if filename.endswith('.otf'):
name = filename.split('.otf')[0]
fonts[name] = os.path.join(path, filename)
if filename.endswith('.ttf'):
name = filename.split('.ttf')[0]
fonts[name] = os.path.join(path, filename)
if filename.endswith('.ttf'):
name = filename.split('.ttf')[0]
fonts[name] = os.path.join(path, filename)
available_fonts = [key for key, values in fonts.items()]
available_fonts = [key for key,values in fonts.items()]
def get_fonts():
"""Print all available fonts by name.
"""Print all available fonts by name.
Searches the /font folder in Inkycal and displays all fonts found in
there.
Searches the /font folder in Inkycal and displays all fonts found in
there.
Returns:
printed output of all available fonts. To access a fontfile, use the
fonts dictionary to access it.
Returns:
printed output of all available fonts. To access a fontfile, use the
fonts dictionary to access it.
>>> fonts['fontname']
>>> fonts['fontname']
To use a font, use the following sytax, where fontname is one of the
printed fonts of this function:
To use a font, use the following sytax, where fontname is one of the
printed fonts of this function:
>>> ImageFont.truetype(fonts['fontname'], size = 10)
"""
for fonts in available_fonts:
print(fonts)
>>> ImageFont.truetype(fonts['fontname'], size = 10)
"""
for fonts in available_fonts:
print(fonts)
def get_system_tz():
"""Gets the system-timezone
"""Gets the system-timezone
Gets the timezone set by the system.
Gets the timezone set by the system.
Returns:
- A timezone if a system timezone was found.
- None if no timezone was found.
Returns:
- A timezone if a system timezone was found.
- None if no timezone was found.
The extracted timezone can be used to show the local time instead of UTC. e.g.
The extracted timezone can be used to show the local time instead of UTC. e.g.
>>> import arrow
>>> print(arrow.now()) # returns non-timezone-aware time
>>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
"""
try:
local_tz = time.tzname[1]
except:
print('System timezone could not be parsed!')
print('Please set timezone manually!. Setting timezone to None...')
local_tz = None
return local_tz
>>> import arrow
>>> print(arrow.now()) # returns non-timezone-aware time
>>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time.
"""
try:
local_tz = time.tzname[1]
except:
print('System timezone could not be parsed!')
print('Please set timezone manually!. Setting timezone to None...')
local_tz = None
return local_tz
def auto_fontsize(font, max_height):
"""Scales a given font to 80% of max_height.
"""Scales a given font to 80% of max_height.
Gets the height of a font and scales it until 80% of the max_height
is filled.
Gets the height of a font and scales it until 80% of the max_height
is filled.
Args:
- font: A PIL Font object.
- max_height: An integer representing the height to adjust the font to
which the given font should be scaled to.
Args:
- font: A PIL Font object.
- max_height: An integer representing the height to adjust the font to
which the given font should be scaled to.
Returns:
A PIL font object with modified height.
"""
Returns:
A PIL font object with modified height.
"""
fontsize = font.getsize('hg')[1]
while font.getsize('hg')[1] <= (max_height * 0.80):
fontsize += 1
font = ImageFont.truetype(font.path, fontsize)
return font
fontsize = font.getsize('hg')[1]
while font.getsize('hg')[1] <= (max_height * 0.80):
fontsize += 1
font = ImageFont.truetype(font.path, fontsize)
return font
def write(image, xy, box_size, text, font=None, **kwargs):
"""Writes text on a image.
"""Writes text on a image.
Writes given text at given position on the specified image.
Writes given text at given position on the specified image.
Args:
- image: The image to draw this text on, usually im_black or im_colour.
- xy: tuple-> (x,y) representing the x and y co-ordinate.
- box_size: tuple -> (width, height) representing the size of the text box.
- text: string, the actual text to add on the image.
- font: A PIL Font object e.g.
ImageFont.truetype(fonts['fontname'], size = 10).
Args:
- image: The image to draw this text on, usually im_black or im_colour.
- xy: tuple-> (x,y) representing the x and y co-ordinate.
- box_size: tuple -> (width, height) representing the size of the text box.
- text: string, the actual text to add on the image.
- font: A PIL Font object e.g.
ImageFont.truetype(fonts['fontname'], size = 10).
Args: (optional)
- alignment: alignment of the text, use 'center', 'left', 'right'.
- autofit: bool (True/False). Automatically increases fontsize to fill in
as much of the box-height as possible.
- colour: black by default, do not change as it causes issues with rendering
on e-Paper.
- rotation: Rotate the text with the text-box by a given angle anti-clockwise.
- fill_width: Decimal representing a percentage e.g. 0.9 # 90%. Fill a
maximum of 90% of the size of the full width of text-box.
- fill_height: Decimal representing a percentage e.g. 0.9 # 90%. Fill a
maximum of 90% of the size of the full height of the text-box.
"""
allowed_kwargs = ['alignment', 'autofit', 'colour', 'rotation',
'fill_width', 'fill_height']
Args: (optional)
- alignment: alignment of the text, use 'center', 'left', 'right'.
- autofit: bool (True/False). Automatically increases fontsize to fill in
as much of the box-height as possible.
- colour: black by default, do not change as it causes issues with rendering
on e-Paper.
- rotation: Rotate the text with the text-box by a given angle anti-clockwise.
- fill_width: Decimal representing a percentage e.g. 0.9 # 90%. Fill a
maximum of 90% of the size of the full width of text-box.
- fill_height: Decimal representing a percentage e.g. 0.9 # 90%. Fill a
maximum of 90% of the size of the full height of the text-box.
"""
allowed_kwargs = ['alignment', 'autofit', 'colour', 'rotation',
'fill_width', 'fill_height']
# Validate kwargs
for key, value in kwargs.items():
if key not in allowed_kwargs:
print('{0} does not exist'.format(key))
# Validate kwargs
for key, value in kwargs.items():
if key not in allowed_kwargs:
print('{0} does not exist'.format(key))
# Set kwargs if given, it not, use defaults
alignment = kwargs['alignment'] if 'alignment' in kwargs else 'center'
autofit = kwargs['autofit'] if 'autofit' in kwargs else False
fill_width = kwargs['fill_width'] if 'fill_width' in kwargs else 1.0
fill_height = kwargs['fill_height'] if 'fill_height' in kwargs else 0.8
colour = kwargs['colour'] if 'colour' in kwargs else 'black'
rotation = kwargs['rotation'] if 'rotation' in kwargs else None
# Set kwargs if given, it not, use defaults
alignment = kwargs['alignment'] if 'alignment' in kwargs else 'center'
autofit = kwargs['autofit'] if 'autofit' in kwargs else False
fill_width = kwargs['fill_width'] if 'fill_width' in kwargs else 1.0
fill_height = kwargs['fill_height'] if 'fill_height' in kwargs else 0.8
colour = kwargs['colour'] if 'colour' in kwargs else 'black'
rotation = kwargs['rotation'] if 'rotation' in kwargs else None
x,y = xy
box_width, box_height = box_size
x, y = xy
box_width, box_height = box_size
# Increase fontsize to fit specified height and width of text box
if (autofit == True) or (fill_width != 1.0) or (fill_height != 0.8):
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
while (text_width < int(box_width * fill_width) and
text_height < int(box_height * fill_height)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
# Increase fontsize to fit specified height and width of text box
if (autofit == True) or (fill_width != 1.0) or (fill_height != 0.8):
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
while (text_width < int(box_width * fill_width) and
text_height < int(box_height * fill_height)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
# Truncate text if text is too long so it can fit inside the box
if (text_width, text_height) > (box_width, box_height):
logs.debug(('truncating {}'.format(text)))
while (text_width, text_height) > (box_width, box_height):
text = text[0:-1]
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
logs.debug((text))
# Align text to desired position
if alignment == "center" or None:
x = int((box_width / 2) - (text_width / 2))
elif alignment == 'left':
x = 0
elif alignment == 'right':
x = int(box_width - text_width)
y = int((box_height / 2) - (text_height / 2))
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
# Uncomment following two lines, comment out above two lines to show
# red text-box with white text (debugging purposes)
# space = Image.new('RGBA', (box_width, box_height), color= 'red')
# ImageDraw.Draw(space).text((x, y), text, fill='white', font=font)
if rotation != None:
space.rotate(rotation, expand=True)
# Update only region with text (add text with transparent background)
image.paste(space, xy, space)
# Truncate text if text is too long so it can fit inside the box
if (text_width, text_height) > (box_width, box_height):
logs.debug(('truncating {}'.format(text)))
while (text_width, text_height) > (box_width, box_height):
text=text[0:-1]
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
logs.debug((text))
def text_wrap(text, font=None, max_width=None):
"""Splits a very long text into smaller parts
# Align text to desired position
if alignment == "center" or None:
x = int((box_width / 2) - (text_width / 2))
elif alignment == 'left':
x = 0
elif alignment == 'right':
x = int(box_width - text_width)
Splits a long text to smaller lines which can fit in a line with max_width.
Uses a Font object for more accurate calculations.
y = int((box_height / 2) - (text_height / 2))
Args:
- font: A PIL font object which is used to calculate the size.
- max_width: int-> a width in pixels defining the maximum width before
splitting the text into the next chunk.
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
# Uncomment following two lines, comment out above two lines to show
# red text-box with white text (debugging purposes)
#space = Image.new('RGBA', (box_width, box_height), color= 'red')
#ImageDraw.Draw(space).text((x, y), text, fill='white', font=font)
if rotation != None:
space.rotate(rotation, expand = True)
# Update only region with text (add text with transparent background)
image.paste(space, xy, space)
def text_wrap(text, font=None, max_width = None):
"""Splits a very long text into smaller parts
Splits a long text to smaller lines which can fit in a line with max_width.
Uses a Font object for more accurate calculations.
Args:
- font: A PIL font object which is used to calculate the size.
- max_width: int-> a width in pixels defining the maximum width before
splitting the text into the next chunk.
Returns:
A list containing chunked strings of the full text.
"""
lines = []
if font.getsize(text)[0] < max_width:
lines.append(text)
else:
words = text.split(' ')
i = 0
while i < len(words):
line = ''
while i < len(words) and font.getsize(line + words[i])[0] <= max_width:
line = line + words[i] + " "
i += 1
if not line:
line = words[i]
i += 1
lines.append(line)
return lines
Returns:
A list containing chunked strings of the full text.
"""
lines = []
if font.getsize(text)[0] < max_width:
lines.append(text)
else:
words = text.split(' ')
i = 0
while i < len(words):
line = ''
while i < len(words) and font.getsize(line + words[i])[0] <= max_width:
line = line + words[i] + " "
i += 1
if not line:
line = words[i]
i += 1
lines.append(line)
return lines
def internet_available():
"""checks if the internet is available.
"""checks if the internet is available.
Attempts to connect to google.com with a timeout of 5 seconds to check
if the network can be reached.
Attempts to connect to google.com with a timeout of 5 seconds to check
if the network can be reached.
Returns:
- True if connection could be established.
- False if the internet could not be reached.
Returns:
- True if connection could be established.
- False if the internet could not be reached.
Returned output can be used to add a check for internet availability:
Returned output can be used to add a check for internet availability:
>>> if internet_available() == True:
>>> #...do something that requires internet connectivity
"""
>>> if internet_available() == True:
>>> #...do something that requires internet connectivity
"""
try:
urlopen('https://google.com',timeout=5)
return True
except URLError as err:
return False
try:
urlopen('https://google.com', timeout=5)
return True
except:
return False
def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1,0.1)):
"""Draws a border at given coordinates.
def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1, 0.1)):
"""Draws a border at given coordinates.
Args:
- image: The image on which the border should be drawn (usually im_black or
im_colour.
Args:
- image: The image on which the border should be drawn (usually im_black or
im_colour.
- xy: Tuple representing the top-left corner of the border e.g. (32, 100)
where 32 is the x co-ordinate and 100 is the y-coordinate.
- xy: Tuple representing the top-left corner of the border e.g. (32, 100)
where 32 is the x co-ordinate and 100 is the y-coordinate.
- size: Size of the border as a tuple -> (width, height).
- size: Size of the border as a tuple -> (width, height).
- radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.
- radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners.
- thickness: Thickness of the border in pixels.
- thickness: Thickness of the border in pixels.
- shrinkage: A tuple containing decimals presenting a percentage of shrinking
-> (width_shrink_percentage, height_shrink_percentage).
e.g. (0.1, 0.2) ~ shrinks the width of border by 10%, shrinks height of
border by 20%
"""
- shrinkage: A tuple containing decimals presenting a percentage of shrinking
-> (width_shrink_percentage, height_shrink_percentage).
e.g. (0.1, 0.2) ~ shrinks the width of border by 10%, shrinks height of
border by 20%
"""
colour='black'
colour = 'black'
# size from function paramter
width, height = int(size[0]*(1-shrinkage[0])), int(size[1]*(1-shrinkage[1]))
# size from function paramter
width, height = int(size[0] * (1 - shrinkage[0])), int(size[1] * (1 - shrinkage[1]))
# shift cursor to move rectangle to center
offset_x, offset_y = int((size[0] - width)/2), int((size[1]- height)/2)
# shift cursor to move rectangle to center
offset_x, offset_y = int((size[0] - width) / 2), int((size[1] - height) / 2)
x, y, diameter = xy[0]+offset_x, xy[1]+offset_y, radius*2
# lenght of rectangle size
a,b = (width - diameter), (height-diameter)
x, y, diameter = xy[0] + offset_x, xy[1] + offset_y, radius * 2
# lenght of rectangle size
a, b = (width - diameter), (height - diameter)
# Set coordinates for staright lines
p1, p2 = (x+radius, y), (x+radius+a, y)
p3, p4 = (x+width, y+radius), (x+width, y+radius+b)
p5, p6 = (p2[0], y+height), (p1[0], y+height)
p7, p8 = (x, p4[1]), (x,p3[1])
if radius != 0:
# Set coordinates for arcs
c1, c2 = (x,y), (x+diameter, y+diameter)
c3, c4 = ((x+width)-diameter, y), (x+width, y+diameter)
c5, c6 = ((x+width)-diameter, (y+height)-diameter), (x+width, y+height)
c7, c8 = (x, (y+height)-diameter), (x+diameter, y+height)
# Set coordinates for staright lines
p1, p2 = (x + radius, y), (x + radius + a, y)
p3, p4 = (x + width, y + radius), (x + width, y + radius + b)
p5, p6 = (p2[0], y + height), (p1[0], y + height)
p7, p8 = (x, p4[1]), (x, p3[1])
if radius != 0:
# Set coordinates for arcs
c1, c2 = (x, y), (x + diameter, y + diameter)
c3, c4 = ((x + width) - diameter, y), (x + width, y + diameter)
c5, c6 = ((x + width) - diameter, (y + height) - diameter), (x + width, y + height)
c7, c8 = (x, (y + height) - diameter), (x + diameter, y + height)
# Draw lines and arcs, creating a square with round corners
draw = ImageDraw.Draw(image)
draw.line( (p1, p2) , fill=colour, width = thickness)
draw.line( (p3, p4) , fill=colour, width = thickness)
draw.line( (p5, p6) , fill=colour, width = thickness)
draw.line( (p7, p8) , fill=colour, width = thickness)
# Draw lines and arcs, creating a square with round corners
draw = ImageDraw.Draw(image)
draw.line((p1, p2), fill=colour, width=thickness)
draw.line((p3, p4), fill=colour, width=thickness)
draw.line((p5, p6), fill=colour, width=thickness)
draw.line((p7, p8), fill=colour, width=thickness)
if radius != 0:
draw.arc( (c1, c2) , 180, 270, fill=colour, width=thickness)
draw.arc( (c3, c4) , 270, 360, fill=colour, width=thickness)
draw.arc( (c5, c6) , 0, 90, fill=colour, width=thickness)
draw.arc( (c7, c8) , 90, 180, fill=colour, width=thickness)
if radius != 0:
draw.arc((c1, c2), 180, 270, fill=colour, width=thickness)
draw.arc((c3, c4), 270, 360, fill=colour, width=thickness)
draw.arc((c5, c6), 0, 90, fill=colour, width=thickness)
draw.arc((c7, c8), 90, 180, fill=colour, width=thickness)

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
9.7" driver class
Copyright by aceisace
@@ -9,42 +9,42 @@ from os.path import exists
from PIL import Image
# Display resolution
EPD_WIDTH = 1200
EPD_HEIGHT = 825
EPD_WIDTH = 1200
EPD_HEIGHT = 825
driver_dir = top_level + '/inkycal/display/drivers/9_in_7_drivers/'
driver_dir = top_level+'/inkycal/display/drivers/9_in_7_drivers/'
class EPD:
def __init__(self):
"""9.7" epaper class"""
# Check if zipped folders are present, if yes, assume
# drivers have not been installed yet
def __init__(self):
"""9.7" epaper class"""
# Check if zipped folders are present, if yes, assume
# drivers have not been installed yet
if exists(f'{driver_dir}IT8951.zip'):
print('Additional steps are required to install drivers for 9.7" E-Paper. '
'Please run the following command in Terminal, then retry:\n'
f'bash {driver_dir}install.sh')
if exists(f'{driver_dir}IT8951.zip'):
print('Additional steps are required to install drivers for 9.7" E-Paper. '
'Please run the following command in Terminal, then retry:\n'
f'bash {driver_dir}install.sh')
def init(self):
pass
def init(self):
pass
def display(self, command):
"""displays an image"""
try:
run_command = command.split()
run(run_command)
except:
print("oops, something didn't work right :/")
def display(self, command):
"""displays an image"""
try:
run_command = command.split()
run(run_command)
except:
print("oops, something didn't work right :/")
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True)
image.convert('RGB').save(images+'canvas.bmp', 'BMP')
command = 'sudo {}IT8951/IT8951 0 0 {}'.format(driver_dir, images+'canvas.bmp')
#print(command)
return command
def sleep(self):
pass
def getbuffer(self, image):
"""ad-hoc"""
image = image.rotate(90, expand=True)
image.convert('RGB').save(images + 'canvas.bmp', 'BMP')
command = 'sudo {}IT8951/IT8951 0 0 {}'.format(driver_dir, images + 'canvas.bmp')
# print(command)
return command
def sleep(self):
pass

View File

@@ -34,13 +34,14 @@ from PIL import Image
import RPi.GPIO as GPIO
# Display resolution
EPD_WIDTH = 400
EPD_HEIGHT = 300
EPD_WIDTH = 400
EPD_HEIGHT = 300
GRAY1 = 0xff # white
GRAY2 = 0xC0
GRAY3 = 0x80 # gray
GRAY4 = 0x00 # Blackest
GRAY1 = 0xff #white
GRAY2 = 0xC0
GRAY3 = 0x80 #gray
GRAY4 = 0x00 #Blackest
class EPD:
def __init__(self):
@@ -50,117 +51,117 @@ class EPD:
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
self.GRAY1 = GRAY1 #white
self.GRAY2 = GRAY2
self.GRAY3 = GRAY3 #gray
self.GRAY4 = GRAY4 #Blackest
self.GRAY1 = GRAY1 # white
self.GRAY2 = GRAY2
self.GRAY3 = GRAY3 # gray
self.GRAY4 = GRAY4 # Blackest
lut_vcom0 = [
0x00, 0x17, 0x00, 0x00, 0x00, 0x02,
0x00, 0x17, 0x17, 0x00, 0x00, 0x02,
0x00, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x17, 0x00, 0x00, 0x00, 0x02,
0x00, 0x17, 0x17, 0x00, 0x00, 0x02,
0x00, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_ww = [
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_bw = [
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x40, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x40, 0x0A, 0x01, 0x00, 0x00, 0x01,
0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_wb = [
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
lut_bb = [
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x80, 0x17, 0x00, 0x00, 0x00, 0x02,
0x90, 0x17, 0x17, 0x00, 0x00, 0x02,
0x80, 0x0A, 0x01, 0x00, 0x00, 0x01,
0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
#******************************gray*********************************/
#0~3 gray
EPD_4IN2_4Gray_lut_vcom =[
0x00 ,0x0A ,0x00 ,0x00 ,0x00 ,0x01,
0x60 ,0x14 ,0x14 ,0x00 ,0x00 ,0x01,
0x00 ,0x14 ,0x00 ,0x00 ,0x00 ,0x01,
0x00 ,0x13 ,0x0A ,0x01 ,0x00 ,0x01,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00
# ******************************gray*********************************/
# 0~3 gray
EPD_4IN2_4Gray_lut_vcom = [
0x00, 0x0A, 0x00, 0x00, 0x00, 0x01,
0x60, 0x14, 0x14, 0x00, 0x00, 0x01,
0x00, 0x14, 0x00, 0x00, 0x00, 0x01,
0x00, 0x13, 0x0A, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]
#R21
EPD_4IN2_4Gray_lut_ww =[
0x40 ,0x0A ,0x00 ,0x00 ,0x00 ,0x01,
0x90 ,0x14 ,0x14 ,0x00 ,0x00 ,0x01,
0x10 ,0x14 ,0x0A ,0x00 ,0x00 ,0x01,
0xA0 ,0x13 ,0x01 ,0x00 ,0x00 ,0x01,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
# R21
EPD_4IN2_4Gray_lut_ww = [
0x40, 0x0A, 0x00, 0x00, 0x00, 0x01,
0x90, 0x14, 0x14, 0x00, 0x00, 0x01,
0x10, 0x14, 0x0A, 0x00, 0x00, 0x01,
0xA0, 0x13, 0x01, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
#R22H r
EPD_4IN2_4Gray_lut_bw =[
0x40 ,0x0A ,0x00 ,0x00 ,0x00 ,0x01,
0x90 ,0x14 ,0x14 ,0x00 ,0x00 ,0x01,
0x00 ,0x14 ,0x0A ,0x00 ,0x00 ,0x01,
0x99 ,0x0C ,0x01 ,0x03 ,0x04 ,0x01,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
# R22H r
EPD_4IN2_4Gray_lut_bw = [
0x40, 0x0A, 0x00, 0x00, 0x00, 0x01,
0x90, 0x14, 0x14, 0x00, 0x00, 0x01,
0x00, 0x14, 0x0A, 0x00, 0x00, 0x01,
0x99, 0x0C, 0x01, 0x03, 0x04, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
#R23H w
EPD_4IN2_4Gray_lut_wb =[
0x40 ,0x0A ,0x00 ,0x00 ,0x00 ,0x01,
0x90 ,0x14 ,0x14 ,0x00 ,0x00 ,0x01,
0x00 ,0x14 ,0x0A ,0x00 ,0x00 ,0x01,
0x99 ,0x0B ,0x04 ,0x04 ,0x01 ,0x01,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
# R23H w
EPD_4IN2_4Gray_lut_wb = [
0x40, 0x0A, 0x00, 0x00, 0x00, 0x01,
0x90, 0x14, 0x14, 0x00, 0x00, 0x01,
0x00, 0x14, 0x0A, 0x00, 0x00, 0x01,
0x99, 0x0B, 0x04, 0x04, 0x01, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
#R24H b
EPD_4IN2_4Gray_lut_bb =[
0x80 ,0x0A ,0x00 ,0x00 ,0x00 ,0x01,
0x90 ,0x14 ,0x14 ,0x00 ,0x00 ,0x01,
0x20 ,0x14 ,0x0A ,0x00 ,0x00 ,0x01,
0x50 ,0x13 ,0x01 ,0x00 ,0x00 ,0x01,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00,
# R24H b
EPD_4IN2_4Gray_lut_bb = [
0x80, 0x0A, 0x00, 0x00, 0x00, 0x01,
0x90, 0x14, 0x14, 0x00, 0x00, 0x01,
0x20, 0x14, 0x0A, 0x00, 0x00, 0x01,
0x50, 0x13, 0x01, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(10)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -173,306 +174,309 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
self.send_command(0x71)
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
while (epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
self.send_command(0x71)
epdconfig.delay_ms(100)
epdconfig.delay_ms(100)
def set_lut(self):
self.send_command(0x20) # vcom
self.send_command(0x20) # vcom
for count in range(0, 44):
self.send_data(self.lut_vcom0[count])
self.send_command(0x21) # ww --
self.send_command(0x21) # ww --
for count in range(0, 42):
self.send_data(self.lut_ww[count])
self.send_command(0x22) # bw r
self.send_command(0x22) # bw r
for count in range(0, 42):
self.send_data(self.lut_bw[count])
self.send_command(0x23) # wb w
self.send_command(0x23) # wb w
for count in range(0, 42):
self.send_data(self.lut_bb[count])
self.send_command(0x24) # bb b
self.send_command(0x24) # bb b
for count in range(0, 42):
self.send_data(self.lut_wb[count])
def Gray_SetLut(self):
self.send_command(0x20) #vcom
self.send_command(0x20) # vcom
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_vcom[count])
self.send_data(self.EPD_4IN2_4Gray_lut_vcom[count])
self.send_command(0x21) #red not use
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_ww[count])
self.send_command(0x22) #bw r
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_bw[count])
self.send_command(0x23) #wb w
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_wb[count])
self.send_command(0x24) #bb b
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_bb[count])
self.send_command(0x25) #vcom
self.send_command(0x21) # red not use
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_ww[count])
self.send_command(0x22) # bw r
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_bw[count])
self.send_command(0x23) # wb w
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_wb[count])
self.send_command(0x24) # bb b
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_bb[count])
self.send_command(0x25) # vcom
for count in range(0, 42):
self.send_data(self.EPD_4IN2_4Gray_lut_ww[count])
def init(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.send_command(0x01) # POWER SETTING
self.send_data(0x03) # VDS_EN, VDG_EN
self.send_data(0x00) # VCOM_HV, VGHL_LV[1], VGHL_LV[0]
self.send_data(0x2b) # VDH
self.send_data(0x2b) # VDL
self.send_command(0x06) # boost soft start
self.send_data(0x17)
self.send_data(0x17)
self.send_data(0x17)
self.send_command(0x04) # POWER_ON
self.ReadBusy()
self.send_command(0x00) # panel setting
self.send_data(0xbf) # KW-BF KWR-AF BWROTP 0f
self.send_data(0x0d)
self.send_command(0x30) # PLL setting
self.send_data(0x3c) # 3A 100HZ 29 150Hz 39 200HZ 31 171HZ
self.send_command(0x61) # resolution setting
self.send_command(0x01) # POWER SETTING
self.send_data(0x03) # VDS_EN, VDG_EN
self.send_data(0x00) # VCOM_HV, VGHL_LV[1], VGHL_LV[0]
self.send_data(0x2b) # VDH
self.send_data(0x2b) # VDL
self.send_command(0x06) # boost soft start
self.send_data(0x17)
self.send_data(0x17)
self.send_data(0x17)
self.send_command(0x04) # POWER_ON
self.ReadBusy()
self.send_command(0x00) # panel setting
self.send_data(0xbf) # KW-BF KWR-AF BWROTP 0f
self.send_data(0x0d)
self.send_command(0x30) # PLL setting
self.send_data(0x3c) # 3A 100HZ 29 150Hz 39 200HZ 31 171HZ
self.send_command(0x61) # resolution setting
self.send_data(0x01)
self.send_data(0x90) # 128
self.send_data(0x01)
self.send_data(0x90) # 128
self.send_data(0x01)
self.send_data(0x2c)
self.send_command(0x82) # vcom_DC setting
self.send_command(0x82) # vcom_DC setting
self.send_data(0x28)
self.send_command(0X50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x97) # 97white border 77black border VBDF 17|D7 VBDW 97 VBDB 57 VBDF F7 VBDW 77 VBDB 37 VBDR B7
self.send_command(0X50) # VCOM AND DATA INTERVAL SETTING
self.send_data(
0x97) # 97white border 77black border VBDF 17|D7 VBDW 97 VBDB 57 VBDF F7 VBDW 77 VBDB 37 VBDR B7
self.set_lut()
# EPD hardware init end
return 0
def Init_4Gray(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.send_command(0x01) #POWER SETTING
self.send_data (0x03)
self.send_data (0x00) #VGH=20V,VGL=-20V
self.send_data (0x2b) #VDH=15V
self.send_data (0x2b) #VDL=-15V
self.send_data (0x13)
self.send_command(0x06) #booster soft start
self.send_data (0x17) #A
self.send_data (0x17) #B
self.send_data (0x17) #C
self.send_command(0x01) # POWER SETTING
self.send_data(0x03)
self.send_data(0x00) # VGH=20V,VGL=-20V
self.send_data(0x2b) # VDH=15V
self.send_data(0x2b) # VDL=-15V
self.send_data(0x13)
self.send_command(0x06) # booster soft start
self.send_data(0x17) # A
self.send_data(0x17) # B
self.send_data(0x17) # C
self.send_command(0x04)
self.ReadBusy()
self.send_command(0x00) #panel setting
self.send_data(0x3f) #KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.send_command(0x00) # panel setting
self.send_data(0x3f) # KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.send_command(0x30) #PLL setting
self.send_data (0x3c) #100hz
self.send_command(0x30) # PLL setting
self.send_data(0x3c) # 100hz
self.send_command(0x61) #resolution setting
self.send_data (0x01) #400
self.send_data (0x90)
self.send_data (0x01) #300
self.send_data (0x2c)
self.send_command(0x61) # resolution setting
self.send_data(0x01) # 400
self.send_data(0x90)
self.send_data(0x01) # 300
self.send_data(0x2c)
self.send_command(0x82) #vcom_DC setting
self.send_data (0x12)
self.send_command(0x82) # vcom_DC setting
self.send_data(0x12)
self.send_command(0X50) #VCOM AND DATA INTERVAL SETTING
self.send_command(0X50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x97)
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
if(imwidth == self.width and imheight == self.height):
if (imwidth == self.width and imheight == self.height):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def getbuffer_4Gray(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 4) * self.height)
image_monocolor = image.convert('L')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
i=0
i = 0
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
if(imwidth == self.width and imheight == self.height):
if (imwidth == self.width and imheight == self.height):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if(pixels[x, y] == 0xC0):
if (pixels[x, y] == 0xC0):
pixels[x, y] = 0x80
elif (pixels[x, y] == 0x80):
pixels[x, y] = 0x40
i= i+1
if(i%4 == 0):
buf[int((x + (y * self.width))/4)] = ((pixels[x-3, y]&0xc0) | (pixels[x-2, y]&0xc0)>>2 | (pixels[x-1, y]&0xc0)>>4 | (pixels[x, y]&0xc0)>>6)
elif(imwidth == self.height and imheight == self.width):
i = i + 1
if (i % 4 == 0):
buf[int((x + (y * self.width)) / 4)] = (
(pixels[x - 3, y] & 0xc0) | (pixels[x - 2, y] & 0xc0) >> 2 | (
pixels[x - 1, y] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6)
elif (imwidth == self.height and imheight == self.width):
logging.debug("Horizontal")
for x in range(imwidth):
for y in range(imheight):
newx = y
newy = x
if(pixels[x, y] == 0xC0):
if (pixels[x, y] == 0xC0):
pixels[x, y] = 0x80
elif (pixels[x, y] == 0x80):
pixels[x, y] = 0x40
i= i+1
if(i%4 == 0):
buf[int((newx + (newy * self.width))/4)] = ((pixels[x, y-3]&0xc0) | (pixels[x, y-2]&0xc0)>>2 | (pixels[x, y-1]&0xc0)>>4 | (pixels[x, y]&0xc0)>>6)
i = i + 1
if (i % 4 == 0):
buf[int((newx + (newy * self.width)) / 4)] = (
(pixels[x, y - 3] & 0xc0) | (pixels[x, y - 2] & 0xc0) >> 2 | (
pixels[x, y - 1] & 0xc0) >> 4 | (pixels[x, y] & 0xc0) >> 6)
return buf
def display(self, image):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(image[i])
self.send_command(0x12)
self.send_command(0x12)
self.ReadBusy()
def display_4Gray(self, image):
self.send_command(0x10)
for i in range(0, EPD_WIDTH * EPD_HEIGHT / 8): # EPD_WIDTH * EPD_HEIGHT / 4
temp3=0
for i in range(0, EPD_WIDTH * EPD_HEIGHT / 8): # EPD_WIDTH * EPD_HEIGHT / 4
temp3 = 0
for j in range(0, 2):
temp1 = image[i*2+j]
temp1 = image[i * 2 + j]
for k in range(0, 2):
temp2 = temp1&0xC0
if(temp2 == 0xC0):
temp3 |= 0x01#white
elif(temp2 == 0x00):
temp3 |= 0x00 #black
elif(temp2 == 0x80):
temp3 |= 0x01 #gray1
else: #0x40
temp3 |= 0x00 #gray2
temp3 <<= 1
temp2 = temp1 & 0xC0
if (temp2 == 0xC0):
temp3 |= 0x01 # white
elif (temp2 == 0x00):
temp3 |= 0x00 # black
elif (temp2 == 0x80):
temp3 |= 0x01 # gray1
else: # 0x40
temp3 |= 0x00 # gray2
temp3 <<= 1
temp1 <<= 2
temp2 = temp1&0xC0
if(temp2 == 0xC0): #white
temp2 = temp1 & 0xC0
if (temp2 == 0xC0): # white
temp3 |= 0x01
elif(temp2 == 0x00): #black
elif (temp2 == 0x00): # black
temp3 |= 0x00
elif(temp2 == 0x80):
temp3 |= 0x01 #gray1
else : #0x40
temp3 |= 0x00 #gray2
if(j!=1 or k!=1):
elif (temp2 == 0x80):
temp3 |= 0x01 # gray1
else: # 0x40
temp3 |= 0x00 # gray2
if (j != 1 or k != 1):
temp3 <<= 1
temp1 <<= 2
self.send_data(temp3)
self.send_command(0x13)
for i in range(0, EPD_WIDTH * EPD_HEIGHT / 8): #5808*4 46464
temp3=0
self.send_command(0x13)
for i in range(0, EPD_WIDTH * EPD_HEIGHT / 8): # 5808*4 46464
temp3 = 0
for j in range(0, 2):
temp1 = image[i*2+j]
temp1 = image[i * 2 + j]
for k in range(0, 2):
temp2 = temp1&0xC0
if(temp2 == 0xC0):
temp3 |= 0x01#white
elif(temp2 == 0x00):
temp3 |= 0x00 #black
elif(temp2 == 0x80):
temp3 |= 0x00 #gray1
else: #0x40
temp3 |= 0x01 #gray2
temp3 <<= 1
temp2 = temp1 & 0xC0
if (temp2 == 0xC0):
temp3 |= 0x01 # white
elif (temp2 == 0x00):
temp3 |= 0x00 # black
elif (temp2 == 0x80):
temp3 |= 0x00 # gray1
else: # 0x40
temp3 |= 0x01 # gray2
temp3 <<= 1
temp1 <<= 2
temp2 = temp1&0xC0
if(temp2 == 0xC0): #white
temp2 = temp1 & 0xC0
if (temp2 == 0xC0): # white
temp3 |= 0x01
elif(temp2 == 0x00): #black
elif (temp2 == 0x00): # black
temp3 |= 0x00
elif(temp2 == 0x80):
temp3 |= 0x00 #gray1
else: #0x40
temp3 |= 0x01 #gray2
if(j!=1 or k!=1):
elif (temp2 == 0x80):
temp3 |= 0x00 # gray1
else: # 0x40
temp3 |= 0x01 # gray2
if (j != 1 or k != 1):
temp3 <<= 1
temp1 <<= 2
self.send_data(temp3)
self.Gray_SetLut()
self.send_command(0x12)
epdconfig.delay_ms(200)
self.ReadBusy()
# pass
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x12)
self.send_command(0x12)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.module_exit()
### END OF FILE ###
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -31,8 +31,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 400
EPD_HEIGHT = 300
EPD_WIDTH = 400
EPD_HEIGHT = 300
class EPD:
def __init__(self):
@@ -46,11 +47,12 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(5) # support v2 displays in favor of v1 displays.Change this to 10 for legacy v1 display support
epdconfig.delay_ms(
5) # support v2 displays in favor of v1 displays.Change this to 10 for legacy v1 display support
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -63,86 +65,85 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
while (epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
logging.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data (0x17)
self.send_data (0x17)
self.send_data (0x17) # 07 0f 17 1f 27 2F 37 2f
self.send_command(0x04) # POWER_ON
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data(0x17)
self.send_data(0x17)
self.send_data(0x17) # 07 0f 17 1f 27 2F 37 2f
self.send_command(0x04) # POWER_ON
self.ReadBusy()
self.send_command(0x00) # PANEL_SETTING
self.send_data(0x0F) # LUT from OTP
self.send_command(0x00) # PANEL_SETTING
self.send_data(0x0F) # LUT from OTP
return 0
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
if(imwidth == self.width and imheight == self.height):
if (imwidth == self.width and imheight == self.height):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, imageblack, imagered):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imageblack[i])
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imagered[i])
self.send_command(0x12)
self.send_command(0x12)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xFF)
self.send_command(0x12)
self.send_command(0x12)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0xA5) # check code
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0xA5) # check code
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 600
EPD_HEIGHT = 448
EPD_WIDTH = 600
EPD_HEIGHT = 448
class EPD:
def __init__(self):
@@ -43,15 +44,15 @@ class EPD:
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(10)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,59 +65,59 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
while (epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
logging.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.send_command(0x01) # POWER_SETTING
self.send_command(0x01) # POWER_SETTING
self.send_data(0x37)
self.send_data(0x00)
self.send_command(0x00) # PANEL_SETTING
self.send_command(0x00) # PANEL_SETTING
self.send_data(0xCF)
self.send_data(0x08)
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x28)
self.send_command(0x04) # POWER_ON
self.send_command(0x04) # POWER_ON
self.ReadBusy()
self.send_command(0x30) # PLL_CONTROL
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3c)
self.send_command(0x41) # TEMPERATURE_CALIBRATION
self.send_command(0x41) # TEMPERATURE_CALIBRATION
self.send_data(0x00)
self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING
self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING
self.send_data(0x77)
self.send_command(0x60) # TCON_SETTING
self.send_command(0x60) # TCON_SETTING
self.send_data(0x22)
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(0x02) # source 600
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(0x02) # source 600
self.send_data(0x58)
self.send_data(0x01) # gate 448
self.send_data(0x01) # gate 448
self.send_data(0xC0)
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x1E) # decide by LUT file
self.send_command(0xe5) # FLASH MODE
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x1E) # decide by LUT file
self.send_command(0xe5) # FLASH MODE
self.send_data(0x03)
# EPD hardware init end
return 0
@@ -125,30 +126,30 @@ class EPD:
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
logging.debug('imwidth = %d imheight = %d ',imwidth, imheight)
if(imwidth == self.width and imheight == self.height):
logging.debug('imwidth = %d imheight = %d ', imwidth, imheight)
if (imwidth == self.width and imheight == self.height):
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] < 64: # black
if pixels[x, y] < 64: # black
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
elif pixels[x, y] < 192: # convert gray to red
elif pixels[x, y] < 192: # convert gray to red
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2)
else: # white
else: # white
buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2)
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] < 64: # black
buf[int((newx + newy*self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
elif pixels[x, y] < 192: # convert gray to red
buf[int((newx + newy*self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
buf[int((newx + newy*self.width) / 4)] |= 0x40 >> (y % 4 * 2)
else: # white
buf[int((newx + newy*self.width) / 4)] |= 0xC0 >> (y % 4 * 2)
newy = self.height - x - 1
if pixels[x, y] < 64: # black
buf[int((newx + newy * self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
elif pixels[x, y] < 192: # convert gray to red
buf[int((newx + newy * self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
buf[int((newx + newy * self.width) / 4)] |= 0x40 >> (y % 4 * 2)
else: # white
buf[int((newx + newy * self.width) / 4)] |= 0xC0 >> (y % 4 * 2)
return buf
def display(self, image):
@@ -166,7 +167,7 @@ class EPD:
temp2 = (temp2 << 4) & 0xFF
temp1 = (temp1 << 2) & 0xFF
j += 1
if((temp1 & 0xC0) == 0xC0):
if ((temp1 & 0xC0) == 0xC0):
temp2 |= 0x03
elif ((temp1 & 0xC0) == 0x00):
temp2 |= 0x00
@@ -175,11 +176,11 @@ class EPD:
temp1 = (temp1 << 2) & 0xFF
self.send_data(temp2)
j += 1
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width / 4 * self.height)):
@@ -189,12 +190,11 @@ class EPD:
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.module_exit()
### END OF FILE ###
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 600
EPD_HEIGHT = 448
EPD_WIDTH = 600
EPD_HEIGHT = 448
class EPD:
def __init__(self):
@@ -47,11 +48,11 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(10)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,80 +65,80 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
while (epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
logging.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.send_command(0x01) # POWER_SETTING
self.send_command(0x01) # POWER_SETTING
self.send_data(0x37)
self.send_data(0x00)
self.send_command(0x00) # PANEL_SETTING
self.send_command(0x00) # PANEL_SETTING
self.send_data(0xCF)
self.send_data(0x08)
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3A) # PLL: 0-15:0x3C, 15+:0x3A
self.send_command(0X82) # VCOM VOLTAGE SETTING
self.send_data(0x28) # all temperature range
self.send_command(0x06) # boost
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x15)
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3A) # PLL: 0-15:0x3C, 15+:0x3A
self.send_command(0X82) # VCOM VOLTAGE SETTING
self.send_data(0x28) # all temperature range
self.send_command(0X50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x77)
self.send_command(0x06) # boost
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x15)
self.send_command(0X60) # TCON SETTING
self.send_data(0x22)
self.send_command(0X50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x77)
self.send_command(0X65) # FLASH CONTROL
self.send_command(0X60) # TCON SETTING
self.send_data(0x22)
self.send_command(0X65) # FLASH CONTROL
self.send_data(0x00)
self.send_command(0x61) # tres
self.send_data(0x02) # source 600
self.send_data(0x58)
self.send_data(0x01) # gate 448
self.send_command(0x61) # tres
self.send_data(0x02) # source 600
self.send_data(0x58)
self.send_data(0x01) # gate 448
self.send_data(0xc0)
self.send_command(0xe5) # FLASH MODE
self.send_data(0x03)
self.send_command(0xe5) # FLASH MODE
self.send_data(0x03)
self.send_data(0x03)
return 0
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
logging.debug('imwidth = %d imheight = %d ',imwidth, imheight)
if(imwidth == self.width and imheight == self.height):
logging.debug('imwidth = %d imheight = %d ', imwidth, imheight)
if (imwidth == self.width and imheight == self.height):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, imageblack, imagered):
@@ -148,33 +149,33 @@ class EPD:
j = 0
while (j < 8):
if ((temp2 & 0x80) == 0x00):
temp3 = 0x04 #red
temp3 = 0x04 # red
elif ((temp1 & 0x80) == 0x00):
temp3 = 0x00 #black
temp3 = 0x00 # black
else:
temp3 = 0x03 #white
temp3 = 0x03 # white
temp3 = (temp3 << 4) & 0xFF
temp1 = (temp1 << 1) & 0xFF
temp2 = (temp2 << 1) & 0xFF
j += 1
if((temp2 & 0x80) == 0x00):
temp3 |= 0x04 #red
if ((temp2 & 0x80) == 0x00):
temp3 |= 0x04 # red
elif ((temp1 & 0x80) == 0x00):
temp3 |= 0x00 #black
temp3 |= 0x00 # black
else:
temp3 |= 0x03 #white
temp3 |= 0x03 # white
temp1 = (temp1 << 1) & 0xFF
temp2 = (temp2 << 1) & 0xFF
self.send_data(temp3)
j += 1
self.send_command(0x04) # POWER ON
self.send_command(0x04) # POWER ON
self.ReadBusy()
self.send_command(0x12) # display refresh
self.send_command(0x12) # display refresh
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width / 8 * self.height)):
@@ -182,19 +183,18 @@ class EPD:
self.send_data(0x33)
self.send_data(0x33)
self.send_data(0x33)
self.send_command(0x04) # POWER ON
self.send_command(0x04) # POWER ON
self.ReadBusy()
self.send_command(0x12) # display refresh
self.send_command(0x12) # display refresh
epdconfig.delay_ms(100)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0xA5) # check code
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0xA5) # check code
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 640
EPD_HEIGHT = 384
EPD_WIDTH = 640
EPD_HEIGHT = 384
class EPD:
def __init__(self):
@@ -43,15 +44,15 @@ class EPD:
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(10)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,59 +65,59 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
while (epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
logging.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.send_command(0x01) # POWER_SETTING
self.send_command(0x01) # POWER_SETTING
self.send_data(0x37)
self.send_data(0x00)
self.send_command(0x00) # PANEL_SETTING
self.send_command(0x00) # PANEL_SETTING
self.send_data(0xCF)
self.send_data(0x08)
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x28)
self.send_command(0x04) # POWER_ON
self.send_command(0x04) # POWER_ON
self.ReadBusy()
self.send_command(0x30) # PLL_CONTROL
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3c)
self.send_command(0x41) # TEMPERATURE_CALIBRATION
self.send_command(0x41) # TEMPERATURE_CALIBRATION
self.send_data(0x00)
self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING
self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING
self.send_data(0x77)
self.send_command(0x60) # TCON_SETTING
self.send_command(0x60) # TCON_SETTING
self.send_data(0x22)
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(EPD_WIDTH >> 8) #source 640
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(EPD_WIDTH >> 8) # source 640
self.send_data(EPD_WIDTH & 0xff)
self.send_data(EPD_HEIGHT >> 8) #gate 384
self.send_data(EPD_HEIGHT >> 8) # gate 384
self.send_data(EPD_HEIGHT & 0xff)
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x1E) # decide by LUT file
self.send_command(0xe5) # FLASH MODE
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x1E) # decide by LUT file
self.send_command(0xe5) # FLASH MODE
self.send_data(0x03)
# EPD hardware init end
return 0
@@ -126,32 +127,32 @@ class EPD:
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
logging.debug('imwidth = %d imheight = %d ',imwidth, imheight)
if(imwidth == self.width and imheight == self.height):
logging.debug('imwidth = %d imheight = %d ', imwidth, imheight)
if (imwidth == self.width and imheight == self.height):
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] < 64: # black
if pixels[x, y] < 64: # black
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
elif pixels[x, y] < 192: # convert gray to red
elif pixels[x, y] < 192: # convert gray to red
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2)
else: # white
else: # white
buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2)
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] < 64: # black
buf[int((newx + newy*self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
elif pixels[x, y] < 192: # convert gray to red
buf[int((newx + newy*self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
buf[int((newx + newy*self.width) / 4)] |= 0x40 >> (y % 4 * 2)
else: # white
buf[int((newx + newy*self.width) / 4)] |= 0xC0 >> (y % 4 * 2)
return buf
newy = self.height - x - 1
if pixels[x, y] < 64: # black
buf[int((newx + newy * self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
elif pixels[x, y] < 192: # convert gray to red
buf[int((newx + newy * self.width) / 4)] &= ~(0xC0 >> (y % 4 * 2))
buf[int((newx + newy * self.width) / 4)] |= 0x40 >> (y % 4 * 2)
else: # white
buf[int((newx + newy * self.width) / 4)] |= 0xC0 >> (y % 4 * 2)
return buf
def display(self, image):
self.send_command(0x10)
for i in range(0, int(self.width / 4 * self.height)):
@@ -167,7 +168,7 @@ class EPD:
temp2 = (temp2 << 4) & 0xFF
temp1 = (temp1 << 2) & 0xFF
j += 1
if((temp1 & 0xC0) == 0xC0):
if ((temp1 & 0xC0) == 0xC0):
temp2 |= 0x03
elif ((temp1 & 0xC0) == 0x00):
temp2 |= 0x00
@@ -176,27 +177,26 @@ class EPD:
temp1 = (temp1 << 2) & 0xFF
self.send_data(temp2)
j += 1
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width / 4 * self.height)):
for j in range(0, 4):
self.send_data(0x33)
self.send_command(0x12)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 640
EPD_HEIGHT = 384
EPD_WIDTH = 640
EPD_HEIGHT = 384
class EPD:
def __init__(self):
@@ -47,11 +48,11 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(10)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,80 +65,80 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
while (epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
epdconfig.delay_ms(100)
logging.debug("e-Paper busy release")
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.send_command(0x01) # POWER_SETTING
self.send_command(0x01) # POWER_SETTING
self.send_data(0x37)
self.send_data(0x00)
self.send_command(0x00) # PANEL_SETTING
self.send_command(0x00) # PANEL_SETTING
self.send_data(0xCF)
self.send_data(0x08)
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3A) # PLL: 0-15:0x3C, 15+:0x3A
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x28) #all temperature range
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_command(0x30) # PLL_CONTROL
self.send_data(0x3A) # PLL: 0-15:0x3C, 15+:0x3A
self.send_command(0x82) # VCM_DC_SETTING
self.send_data(0x28) # all temperature range
self.send_command(0x06) # BOOSTER_SOFT_START
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x15)
self.send_command(0x50) # VCOM AND DATA INTERVAL SETTING
self.send_command(0x50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x77)
self.send_command(0x60) # TCON_SETTING
self.send_command(0x60) # TCON_SETTING
self.send_data(0x22)
self.send_command(0x65) # FLASH CONTROL
self.send_command(0x65) # FLASH CONTROL
self.send_data(0x00)
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(self.width >> 8) # source 640
self.send_command(0x61) # TCON_RESOLUTION
self.send_data(self.width >> 8) # source 640
self.send_data(self.width & 0xff)
self.send_data(self.height >> 8) # gate 384
self.send_data(self.height >> 8) # gate 384
self.send_data(self.height & 0xff)
self.send_command(0xe5) # FLASH MODE
self.send_command(0xe5) # FLASH MODE
self.send_data(0x03)
return 0
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
logging.debug('imwidth = %d imheight = %d ',imwidth, imheight)
if(imwidth == self.width and imheight == self.height):
logging.debug('imwidth = %d imheight = %d ', imwidth, imheight)
if (imwidth == self.width and imheight == self.height):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, imageblack, imagered):
@@ -148,33 +149,33 @@ class EPD:
j = 0
while (j < 8):
if ((temp2 & 0x80) == 0x00):
temp3 = 0x04 #red
temp3 = 0x04 # red
elif ((temp1 & 0x80) == 0x00):
temp3 = 0x00 #black
temp3 = 0x00 # black
else:
temp3 = 0x03 #white
temp3 = 0x03 # white
temp3 = (temp3 << 4) & 0xFF
temp1 = (temp1 << 1) & 0xFF
temp2 = (temp2 << 1) & 0xFF
j += 1
if((temp2 & 0x80) == 0x00):
temp3 |= 0x04 #red
if ((temp2 & 0x80) == 0x00):
temp3 |= 0x04 # red
elif ((temp1 & 0x80) == 0x00):
temp3 |= 0x00 #black
temp3 |= 0x00 # black
else:
temp3 |= 0x03 #white
temp3 |= 0x03 # white
temp1 = (temp1 << 1) & 0xFF
temp2 = (temp2 << 1) & 0xFF
self.send_data(temp3)
j += 1
self.send_command(0x04) # POWER ON
self.send_command(0x04) # POWER ON
self.ReadBusy()
self.send_command(0x12) # display refresh
self.send_command(0x12) # display refresh
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width / 8 * self.height)):
@@ -182,20 +183,19 @@ class EPD:
self.send_data(0x33)
self.send_data(0x33)
self.send_data(0x33)
self.send_command(0x04) # POWER ON
self.send_command(0x04) # POWER ON
self.ReadBusy()
self.send_command(0x12) # display refresh
self.send_command(0x12) # display refresh
epdconfig.delay_ms(100)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 800
EPD_HEIGHT = 480
EPD_WIDTH = 800
EPD_HEIGHT = 480
class EPD:
def __init__(self):
@@ -43,15 +44,15 @@ class EPD:
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(2)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,49 +65,49 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
while(busy == 0):
while (busy == 0):
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
epdconfig.delay_ms(200)
def init(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.send_command(0x01) #POWER SETTING
self.send_data(0x07)
self.send_data(0x07) #VGH=20V,VGL=-20V
self.send_data(0x3f) #VDH=15V
self.send_data(0x3f) #VDL=-15V
self.send_command(0x04) #POWER ON
self.send_command(0x01) # POWER SETTING
self.send_data(0x07)
self.send_data(0x07) # VGH=20V,VGL=-20V
self.send_data(0x3f) # VDH=15V
self.send_data(0x3f) # VDL=-15V
self.send_command(0x04) # POWER ON
epdconfig.delay_ms(100)
self.ReadBusy()
self.send_command(0X00) #PANNEL SETTING
self.send_data(0x1F) #KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.send_command(0X00) # PANNEL SETTING
self.send_data(0x1F) # KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.send_command(0x61) #tres
self.send_data(0x03) #source 800
self.send_command(0x61) # tres
self.send_data(0x03) # source 800
self.send_data(0x20)
self.send_data(0x01) #gate 480
self.send_data(0x01) # gate 480
self.send_data(0xE0)
self.send_command(0X15)
self.send_data(0x00)
self.send_command(0X50) #VCOM AND DATA INTERVAL SETTING
self.send_command(0X50) # VCOM AND DATA INTERVAL SETTING
self.send_data(0x10)
self.send_data(0x07)
self.send_command(0X60) #TCON SETTING
self.send_command(0X60) # TCON SETTING
self.send_data(0x22)
# EPD hardware init end
@@ -114,57 +115,56 @@ class EPD:
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
if(imwidth == self.width and imheight == self.height):
if (imwidth == self.width and imheight == self.height):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, image):
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(~image[i]);
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 800
EPD_HEIGHT = 480
EPD_WIDTH = 800
EPD_HEIGHT = 480
class EPD:
def __init__(self):
@@ -47,11 +48,11 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(4)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,110 +65,109 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
while(busy == 0):
while (busy == 0):
self.send_command(0x71)
busy = epdconfig.digital_read(self.busy_pin)
epdconfig.delay_ms(200)
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.send_command(0x01); #POWER SETTING
self.send_data(0x07);
self.send_data(0x07); #VGH=20V,VGL=-20V
self.send_data(0x3f); #VDH=15V
self.send_data(0x3f); #VDL=-15V
self.send_command(0x04); #POWER ON
self.reset()
self.send_command(0x01); # POWER SETTING
self.send_data(0x07);
self.send_data(0x07); # VGH=20V,VGL=-20V
self.send_data(0x3f); # VDH=15V
self.send_data(0x3f); # VDL=-15V
self.send_command(0x04); # POWER ON
epdconfig.delay_ms(100);
self.ReadBusy();
self.send_command(0X00); #PANNEL SETTING
self.send_data(0x0F); #KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.send_command(0X00); # PANNEL SETTING
self.send_data(0x0F); # KW-3f KWR-2F BWROTP 0f BWOTP 1f
self.send_command(0x61); #tres
self.send_data(0x03); #source 800
self.send_command(0x61); # tres
self.send_data(0x03); # source 800
self.send_data(0x20);
self.send_data(0x01); #gate 480
self.send_data(0x01); # gate 480
self.send_data(0xE0);
self.send_command(0X15);
self.send_data(0x00);
self.send_command(0X50); #VCOM AND DATA INTERVAL SETTING
self.send_command(0X50); # VCOM AND DATA INTERVAL SETTING
self.send_data(0x11);
self.send_data(0x07);
self.send_command(0X60); #TCON SETTING
self.send_command(0X60); # TCON SETTING
self.send_data(0x22);
return 0
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
logging.debug('imwidth = %d imheight = %d ',imwidth, imheight)
if(imwidth == self.width and imheight == self.height):
logging.debug('imwidth = %d imheight = %d ', imwidth, imheight)
if (imwidth == self.width and imheight == self.height):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, imageblack, imagered):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imageblack[i]);
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(~imagered[i]);
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def Clear(self):
self.send_command(0x10)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xff)
self.send_command(0x13)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00)
self.send_command(0x12)
epdconfig.delay_ms(100)
self.ReadBusy()
def sleep(self):
self.send_command(0x02) # POWER_OFF
self.send_command(0x02) # POWER_OFF
self.ReadBusy()
self.send_command(0x07) # DEEP_SLEEP
self.send_command(0x07) # DEEP_SLEEP
self.send_data(0XA5)
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from . import epdconfig
# Display resolution
EPD_WIDTH = 880
EPD_HEIGHT = 528
EPD_WIDTH = 880
EPD_HEIGHT = 528
class EPD:
def __init__(self):
@@ -43,15 +44,15 @@ class EPD:
self.cs_pin = epdconfig.CS_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(2)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,22 +65,22 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
busy = epdconfig.digital_read(self.busy_pin)
while(busy == 1):
while (busy == 1):
busy = epdconfig.digital_read(self.busy_pin)
epdconfig.delay_ms(200)
def init(self):
if (epdconfig.module_init() != 0):
return -1
# EPD hardware init start
self.reset()
self.ReadBusy();
self.send_command(0x12); #SWRESET
self.send_command(0x12); # SWRESET
self.ReadBusy();
self.send_command(0x46); # Auto Write Red RAM
@@ -94,44 +95,42 @@ class EPD:
self.send_data(0xC7);
self.send_data(0xC3);
self.send_data(0xC0);
self.send_data(0x40);
self.send_data(0x40);
self.send_command(0x01); # Set MUX as 527
self.send_data(0xAF);
self.send_data(0x02);
self.send_data(0x01);#0x01
self.send_data(0x01); # 0x01
self.send_command(0x11); # Data entry mode
self.send_data(0x01);
self.send_command(0x44);
self.send_data(0x00); # RAM x address start at 0
self.send_data(0x00);
self.send_data(0x6F);
self.send_data(0x03);
self.send_command(0x45);
self.send_data(0xAF);
self.send_command(0x44);
self.send_data(0x00); # RAM x address start at 0
self.send_data(0x00);
self.send_data(0x6F);
self.send_data(0x03);
self.send_command(0x45);
self.send_data(0xAF);
self.send_data(0x02);
self.send_data(0x00);
self.send_data(0x00);
self.send_data(0x00);
self.send_command(0x3C); # VBD
self.send_data(0x05); # LUT1, for white
self.send_command(0x3C); # VBD
self.send_data(0x05); # LUT1, for white
self.send_command(0x18);
self.send_data(0X80);
self.send_command(0x22);
self.send_data(0XB1); #Load Temperature and waveform setting.
self.send_data(0XB1); # Load Temperature and waveform setting.
self.send_command(0x20);
self.ReadBusy();
self.send_command(0x4E); # set RAM x address count to 0;
self.send_command(0x4E); # set RAM x address count to 0;
self.send_data(0x00);
self.send_data(0x00);
self.send_command(0x4F);
self.send_command(0x4F);
self.send_data(0x00);
self.send_data(0x00);
# EPD hardware init end
@@ -139,56 +138,56 @@ class EPD:
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
# logging.debug("imwidth = %d, imheight = %d",imwidth,imheight)
if(imwidth == self.width and imheight == self.height):
if (imwidth == self.width and imheight == self.height):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, image):
self.send_command(0x4F);
self.send_command(0x4F);
self.send_data(0x00);
self.send_data(0x00);
self.send_command(0x24);
for i in range(0, int(self.width * self.height / 8)):
self.send_data(image[i]);
self.send_command(0x22);
self.send_data(0xF7);#Load LUT from MCU(0x32)
self.send_data(0xF7); # Load LUT from MCU(0x32)
self.send_command(0x20);
epdconfig.delay_ms(10);
self.ReadBusy();
def Clear(self):
self.send_command(0x4F);
self.send_command(0x4F);
self.send_data(0x00);
self.send_data(0x00);
self.send_command(0x24)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xff)
self.send_command(0x26)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xff)
self.send_command(0x22);
self.send_data(0xF7);#Load LUT from MCU(0x32)
self.send_data(0xF7); # Load LUT from MCU(0x32)
self.send_command(0x20);
epdconfig.delay_ms(10);
self.ReadBusy();
@@ -196,6 +195,6 @@ class EPD:
def sleep(self):
self.send_command(0x10);
self.send_data(0x01);
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -32,8 +32,9 @@ import logging
from inkycal.display.drivers import epdconfig
# Display resolution
EPD_WIDTH = 880
EPD_HEIGHT = 528
EPD_WIDTH = 880
EPD_HEIGHT = 528
class EPD:
def __init__(self):
@@ -47,11 +48,11 @@ class EPD:
# Hardware reset
def reset(self):
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
epdconfig.digital_write(self.reset_pin, 0)
epdconfig.delay_ms(4)
epdconfig.digital_write(self.reset_pin, 1)
epdconfig.delay_ms(200)
epdconfig.delay_ms(200)
def send_command(self, command):
epdconfig.digital_write(self.dc_pin, 0)
@@ -64,37 +65,37 @@ class EPD:
epdconfig.digital_write(self.cs_pin, 0)
epdconfig.spi_writebyte([data])
epdconfig.digital_write(self.cs_pin, 1)
def ReadBusy(self):
logging.debug("e-Paper busy")
busy = epdconfig.digital_read(self.busy_pin)
while(busy == 1):
while (busy == 1):
busy = epdconfig.digital_read(self.busy_pin)
epdconfig.delay_ms(200)
def init(self):
if (epdconfig.module_init() != 0):
return -1
self.reset()
self.send_command(0x12); #SWRESET
self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal
self.send_command(0x12); # SWRESET
self.ReadBusy(); # waiting for the electronic paper IC to release the idle signal
self.send_command(0x46); # Auto Write RAM
self.send_data(0xF7);
self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal
self.ReadBusy(); # waiting for the electronic paper IC to release the idle signal
self.send_command(0x47); # Auto Write RAM
self.send_data(0xF7);
self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal
self.ReadBusy(); # waiting for the electronic paper IC to release the idle signal
self.send_command(0x0C); # Soft start setting
self.send_data(0xAE);
self.send_data(0xC7);
self.send_data(0xC3);
self.send_data(0xC0);
self.send_data(0x40);
self.send_data(0x40);
self.send_command(0x01); # Set MUX as 527
self.send_data(0xAF);
@@ -105,100 +106,98 @@ class EPD:
self.send_data(0x01);
self.send_command(0x44);
self.send_data(0x00); # RAM x address start at 0
self.send_data(0x00); # RAM x address start at 0
self.send_data(0x00);
self.send_data(0x6F); # RAM x address end at 36Fh -> 879
self.send_data(0x6F); # RAM x address end at 36Fh -> 879
self.send_data(0x03);
self.send_command(0x45);
self.send_data(0xAF); # RAM y address start at 20Fh;
self.send_data(0xAF); # RAM y address start at 20Fh;
self.send_data(0x02);
self.send_data(0x00); # RAM y address end at 00h;
self.send_data(0x00); # RAM y address end at 00h;
self.send_data(0x00);
self.send_command(0x3C); # VBD
self.send_data(0x01); # LUT1, for white
self.send_command(0x3C); # VBD
self.send_data(0x01); # LUT1, for white
self.send_command(0x18);
self.send_data(0X80);
self.send_command(0x22);
self.send_data(0XB1); #Load Temperature and waveform setting.
self.send_data(0XB1); # Load Temperature and waveform setting.
self.send_command(0x20);
self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal
self.ReadBusy(); # waiting for the electronic paper IC to release the idle signal
self.send_command(0x4E);
self.send_command(0x4E);
self.send_data(0x00);
self.send_data(0x00);
self.send_command(0x4F);
self.send_command(0x4F);
self.send_data(0xAF);
self.send_data(0x02);
return 0
def getbuffer(self, image):
# logging.debug("bufsiz = ",int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width/8) * self.height)
buf = [0xFF] * (int(self.width / 8) * self.height)
image_monocolor = image.convert('1')
imwidth, imheight = image_monocolor.size
pixels = image_monocolor.load()
logging.debug('imwidth = %d imheight = %d ',imwidth, imheight)
if(imwidth == self.width and imheight == self.height):
logging.debug('imwidth = %d imheight = %d ', imwidth, imheight)
if (imwidth == self.width and imheight == self.height):
logging.debug("Horizontal")
for y in range(imheight):
for x in range(imwidth):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0:
buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8))
elif(imwidth == self.height and imheight == self.width):
elif (imwidth == self.height and imheight == self.width):
logging.debug("Vertical")
for y in range(imheight):
for x in range(imwidth):
newx = y
newy = self.height - x - 1
if pixels[x, y] == 0:
buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8))
buf[int((newx + newy * self.width) / 8)] &= ~(0x80 >> (y % 8))
return buf
def display(self, imageblack, imagered):
self.send_command(0x4F);
self.send_command(0x4F);
self.send_data(0xAf);
self.send_command(0x24)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(imageblack[i]);
self.send_command(0x26)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(~imagered[i]);
self.send_command(0x22);
self.send_data(0xC7); #Load LUT from MCU(0x32)
self.send_data(0xC7); # Load LUT from MCU(0x32)
self.send_command(0x20);
epdconfig.delay_ms(200); #!!!The delay here is necessary, 200uS at least!!!
epdconfig.delay_ms(200); # !!!The delay here is necessary, 200uS at least!!!
self.ReadBusy();
def Clear(self):
self.send_command(0x4F);
self.send_command(0x4F);
self.send_data(0xAf);
self.send_command(0x24)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0xff);
self.send_command(0x26)
for i in range(0, int(self.width * self.height / 8)):
self.send_data(0x00);
self.send_command(0x22);
self.send_data(0xC7); #Load LUT from MCU(0x32)
self.send_data(0xC7); # Load LUT from MCU(0x32)
self.send_command(0x20);
epdconfig.delay_ms(200); #!!!The delay here is necessary, 200uS at least!!!
epdconfig.delay_ms(200); # !!!The delay here is necessary, 200uS at least!!!
self.ReadBusy();
def sleep(self):
self.send_command(0x10); #deep sleep
self.send_command(0x10); # deep sleep
self.send_data(0x01);
epdconfig.module_exit()
### END OF FILE ###

View File

@@ -35,12 +35,13 @@ import time
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class RaspberryPi:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
def __init__(self):
import spidev
@@ -76,7 +77,7 @@ class RaspberryPi:
def module_exit(self):
logger.debug("spi end")
#self.SPI.close() #removed as it causes some problems
# self.SPI.close() #removed as it causes some problems
logger.debug("close 5V, Module enters 0 power consumption ...")
self.GPIO.output(self.RST_PIN, 0)
@@ -87,10 +88,10 @@ class RaspberryPi:
class JetsonNano:
# Pin definition
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
def __init__(self):
import ctypes
@@ -152,5 +153,4 @@ else:
for func in [x for x in dir(implementation) if not x.startswith('_')]:
setattr(sys.modules[__name__], func, getattr(implementation, func))
### END OF FILE ###

View File

@@ -7,4 +7,4 @@ from .inkycal_image import Inkyimage
from .inkycal_jokes import Jokes
from .inkycal_stocks import Stocks
from .inkycal_slideshow import Slideshow
#from .inkycal_server import Inkyserver
# from .inkycal_server import Inkyserver

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Third party module template (inkycal-compatible module)
@@ -13,7 +13,6 @@ Copyright by aceisace
from inkycal.modules.template import inkycal_module
from inkycal.custom import *
#############################################################################
# Built-in library imports (change as desired)
#############################################################################
@@ -21,7 +20,6 @@ from inkycal.custom import *
# Built-in libraries go here
from random import shuffle
#############################################################################
# External library imports (always use try-except)
#############################################################################
@@ -30,11 +28,10 @@ from random import shuffle
# use try...except ImportError to check if it has been installed
# If it is not found, print a short message on how to install this dependency
try:
import feedparser
import feedparser
except ImportError:
print('feedparser is not installed! Please install with:')
print('pip3 install feedparser')
print('feedparser is not installed! Please install with:')
print('pip3 install feedparser')
#############################################################################
# Filename + logging (do not remove)
@@ -44,6 +41,7 @@ except ImportError:
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
#############################################################################
# Class setup
#############################################################################
@@ -52,181 +50,177 @@ logger = logging.getLogger(filename)
# Avoid naming the class with too long names
class Simple(inkycal_module):
""" Simple Class
Once sentence describing what this module does,
e.g. Display hello world with your name!
"""
""" Simple Class
Once sentence describing what this module does,
e.g. Display hello world with your name!
"""
# 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 = "Simple - say hello world"
# 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 = "Simple - say hello world"
# create a dictionary containing variables which your module must have
# to run correctly, e.g. if your module needs an 'api-key' and a 'name':
requires = {
# A simple text input; users can choose what to enter by keyboard
'api_key': {"label" : "Please enter your api-key from some-website"},
# create a dictionary containing variables which your module must have
# to run correctly, e.g. if your module needs an 'api-key' and a 'name':
requires = {
# A simple text input; users can choose what to enter by keyboard
'api_key': {"label": "Please enter your api-key from some-website"},
# A simple text input; users can choose what to enter by keyboard
'username': {"label": "Please enter a username"},
}
# The format for the above is: |"key_name": {"Desription what this means"},|
# A simple text input; users can choose what to enter by keyboard
'username': {"label": "Please enter a username"},
}
# The format for the above is: |"key_name": {"Desription what this means"},|
# create a dictionary containing variables which your module optionally
# can have to run correctly, e.g. if your module needs has optional
# parameters like: 'api-key' and a 'name':
# create a dictionary containing variables which your module optionally
# can have to run correctly, e.g. if your module needs has optional
# parameters like: 'api-key' and a 'name':
#########################################################################
optional = {
#########################################################################
optional = {
# A simple text input with multiple values separated by a comma
'hobbies': {"label": "What is/are your hobbies? Separate multiple ones "
"with a comma"},
# A simple text input with multiple values separated by a comma
'hobbies': {"label": "What is/are your hobbies? Separate multiple ones "
"with a comma"},
# A simple text input which should be a number
'age': {"label": "What is your age? Please enter a number"},
# A dropdown list variable. This will allow users to select something
# from the list in options. Instead of True/False, you can have
# strings, numbers and other datatypes. Add as many options as you need
'likes_inkycal': {
"label": "Do you like Inkycal?",
"options": [True, False],
},
# A simple text input which should be a number
'age': {"label": "What is your age? Please enter a number"},
# A dropdown list with a fallback value in case the user didn't select
# anything
'show_smiley': {
"label": "Show a smiley next to your name?",
"options": [True, False],
"default": True,
},
}
########################################################################
# A dropdown list variable. This will allow users to select something
# from the list in options. Instead of True/False, you can have
# strings, numbers and other datatypes. Add as many options as you need
'likes_inkycal': {
"label": "Do you like Inkycal?",
"options": [True, False],
},
# Initialise the class (do not remove)
def __init__(self, config):
"""Initialize your module module"""
# A dropdown list with a fallback value in case the user didn't select
# anything
'show_smiley': {
"label": "Show a smiley next to your name?",
"options": [True, False],
"default": True,
},
}
# Initialise this module via the inkycal_module template (required)
super().__init__(config)
########################################################################
config = config['config']
# Initialise the class (do not remove)
def __init__(self, config):
"""Initialize your module module"""
# Check if all required parameters are present
# remove this if your module has no required parameters
for param in self.requires:
if not param in config:
raise Exception('config is missing {}'.format(param))
# Initialise this module via the inkycal_module template (required)
super().__init__(config)
# the web-UI removes any blank space from the input
# It can only output strings or booleans, integers and lists need to be
# converted manually, e.g.
config = config['config']
# if you need a boolean (True/False), no conversion is needed:
self.show_smiley = config['show_smiley']
# Check if all required parameters are present
# remove this if your module has no required parameters
for param in self.requires:
if not param in config:
raise Exception('config is missing {}'.format(param))
# if you need a single word input, like the api-ley, no conversion is needed
self.api_key = config['api_key']
# the web-UI removes any blank space from the input
# It can only output strings or booleans, integers and lists need to be
# converted manually, e.g.
# if you need a integer (number) input, you have to convert this to a int
#-----------------------------------------------------------------------#
# bad example :/
self.age = int( config["age"] )
# Remember age was a optional parameter? What if no age was entered
# and there is no fallback value? Then the age would be None.
# This would cause crashing right here
# good example :)
if config["age"] and isinstance(config["age"], str):
self.age = int( config["age"] )
else:
self.age = 10 # just a joke, no offense
# -> Check if age was entered and if it's a string (entered via web-UI)
# If something was entered for age, convert it to a number
# The else statement is executed when nothing was entered for age
# You could assign a custom value now or print something.
#-----------------------------------------------------------------------#
# if you need a boolean (True/False), no conversion is needed:
self.show_smiley = config['show_smiley']
# if you need a list of words, you have to convert the string to a list
#-----------------------------------------------------------------------#
# good example :)
if config["hobbies"] and isinstance(config["hobbies"], str):
self.hobbies = config["age"].split(",")
# split splits the string on each comma -> gives a list
# even if a single value was entered, it will be converted to a list
else:
self.hobbies = [] # empty list if nothing was entered by user
#-----------------------------------------------------------------------#
# if you need a single word input, like the api-ley, no conversion is needed
self.api_key = config['api_key']
# give an OK message
print(f'{filename} loaded')
# if you need a integer (number) input, you have to convert this to a int
# -----------------------------------------------------------------------#
# bad example :/
self.age = int(config["age"])
# Remember age was a optional parameter? What if no age was entered
# and there is no fallback value? Then the age would be None.
# This would cause crashing right here
#############################################################################
# Validation of module specific parameters (optional) #
#############################################################################
# good example :)
if config["age"] and isinstance(config["age"], str):
self.age = int(config["age"])
else:
self.age = 10 # just a joke, no offense
# -> Check if age was entered and if it's a string (entered via web-UI)
# If something was entered for age, convert it to a number
# The else statement is executed when nothing was entered for age
# You could assign a custom value now or print something.
# -----------------------------------------------------------------------#
def _validate(self):
"""Validate module-specific parameters"""
# Check the type of module-specific parameters
# This function is optional, but useful for debugging.
# if you need a list of words, you have to convert the string to a list
# -----------------------------------------------------------------------#
# good example :)
if config["hobbies"] and isinstance(config["hobbies"], str):
self.hobbies = config["age"].split(",")
# split splits the string on each comma -> gives a list
# even if a single value was entered, it will be converted to a list
else:
self.hobbies = [] # empty list if nothing was entered by user
# -----------------------------------------------------------------------#
# Here, we are checking if do_something (from init) is True/False
if not isinstance(self.age, int):
print(f"age has to be a number, but given value is {self.age}")
# give an OK message
print(f'{filename} loaded')
#############################################################################
# Validation of module specific parameters (optional) #
#############################################################################
#############################################################################
# Generating the image #
#############################################################################
def _validate(self):
"""Validate module-specific parameters"""
# Check the type of module-specific parameters
# This function is optional, but useful for debugging.
def generate_image(self):
"""Generate image for this module"""
# Here, we are checking if do_something (from init) is True/False
if not isinstance(self.age, int):
print(f"age has to be a number, but given value is {self.age}")
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info('image size: {} x {} px'.format(im_width, im_height))
#############################################################################
# Generating the image #
#############################################################################
# Use logger.info(), logger.debug(), logger.warning() to display
# useful information for the developer
logger.info('image size: {} x {} px'.format(im_width, im_height))
def generate_image(self):
"""Generate image for this module"""
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size = im_size, color = 'white')
im_colour = Image.new('RGB', size = im_size, color = 'white')
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info('image size: {} x {} px'.format(im_width, im_height))
#################################################################
# Use logger.info(), logger.debug(), logger.warning() to display
# useful information for the developer
logger.info('image size: {} x {} px'.format(im_width, im_height))
# Your code goes here #
# Write/Draw something on the image
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# You can use these custom functions to help you create the image:
# - write() -> write text on the image
# - get_fonts() -> see which fonts are available
# - get_system_tz() -> Get the system's current timezone
# - auto_fontsize() -> Scale the fontsize to the provided height
# - textwrap() -> Split a paragraph into smaller lines
# - internet_available() -> Check if internet is available
# - draw_border() -> Draw a border around the specified area
#################################################################
# If these aren't enough, take a look at python Pillow (imaging library)'s
# documentation.
# Your code goes here #
#################################################################
# Write/Draw something on the image
# return the images ready for the display
return im_black, im_colour
# You can use these custom functions to help you create the image:
# - write() -> write text on the image
# - get_fonts() -> see which fonts are available
# - get_system_tz() -> Get the system's current timezone
# - auto_fontsize() -> Scale the fontsize to the provided height
# - textwrap() -> Split a paragraph into smaller lines
# - internet_available() -> Check if internet is available
# - draw_border() -> Draw a border around the specified area
# If these aren't enough, take a look at python Pillow (imaging library)'s
# documentation.
#################################################################
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print('running {0} in standalone mode'.format(filename))
print('running {0} in standalone mode'.format(filename))
################################################################################
# Last steps
@@ -248,4 +242,3 @@ if __name__ == '__main__':
# How do I now import my module?
# from inkycal.modules import Class
# Where Class is the name of the class inside your module (e.g. Simple)

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
iCalendar (parsing) module for Inky-Calendar Project
Copyright by aceisace
@@ -20,199 +20,196 @@ import time
import os
try:
import recurring_ical_events
import recurring_ical_events
except ModuleNotFoundError:
print('recurring-ical-events library could not be found.')
print('Please install this with: pip3 install recurring-ical-events')
print('recurring-ical-events library could not be found.')
print('Please install this with: pip3 install recurring-ical-events')
try:
from icalendar import Calendar, Event
from icalendar import Calendar, Event
except ModuleNotFoundError:
print('icalendar library could not be found. Please install this with:')
print('pip3 install icalendar')
print('icalendar library could not be found. Please install this with:')
print('pip3 install icalendar')
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class iCalendar:
"""iCalendar parsing moudule for inkycal.
Parses events from given iCalendar URLs / paths"""
"""iCalendar parsing moudule for inkycal.
Parses events from given iCalendar URLs / paths"""
def __init__(self):
self.icalendars = []
self.parsed_events = []
def __init__(self):
self.icalendars = []
self.parsed_events = []
def load_url(self, url, username=None, password=None):
"""Input a string or list of strings containing valid iCalendar URLs
example: 'URL1' (single url) OR ['URL1', 'URL2'] (multiple URLs)
add username and password to access protected files
"""
def load_url(self, url, username=None, password=None):
"""Input a string or list of strings containing valid iCalendar URLs
example: 'URL1' (single url) OR ['URL1', 'URL2'] (multiple URLs)
add username and password to access protected files
"""
if type(url) == list:
if (username == None) and (password == None):
ical = [Calendar.from_ical(str(urlopen(_).read().decode()))
for _ in url]
else:
ical = [auth_ical(each_url, username, password) for each_url in url]
elif type(url) == str:
if (username == None) and (password == None):
ical = [Calendar.from_ical(str(urlopen(url).read().decode()))]
else:
ical = [auth_ical(url, username, password)]
else:
raise Exception (f"Input: '{url}' is not a string or list!")
if type(url) == list:
if (username == None) and (password == None):
ical = [Calendar.from_ical(str(urlopen(_).read().decode()))
for _ in url]
else:
ical = [auth_ical(each_url, username, password) for each_url in url]
elif type(url) == str:
if (username == None) and (password == None):
ical = [Calendar.from_ical(str(urlopen(url).read().decode()))]
else:
ical = [auth_ical(url, username, password)]
else:
raise Exception(f"Input: '{url}' is not a string or list!")
def auth_ical(url, uname, passwd):
"""Authorisation helper for protected ical files"""
def auth_ical(url, uname, passwd):
"""Authorisation helper for protected ical files"""
# Credit to Joshka
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
opener = urllib.request.build_opener(handler)
ical = Calendar.from_ical(str(opener.open(url).read().decode()))
return ical
# Credit to Joshka
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
opener = urllib.request.build_opener(handler)
ical = Calendar.from_ical(str(opener.open(url).read().decode()))
return ical
# Add the parsed icalendar/s to the self.icalendars list
if ical: self.icalendars += ical
logger.info('loaded iCalendars from URLs')
# Add the parsed icalendar/s to the self.icalendars list
if ical: self.icalendars += ical
logger.info('loaded iCalendars from URLs')
def load_from_file(self, filepath):
"""Input a string or list of strings containing valid iCalendar filepaths
example: 'path1' (single file) OR ['path1', 'path2'] (multiple files)
returns a list of iCalendars as string (raw)
"""
if isinstance(filepath, list):
for path in filepath:
with open(path, mode='r') as ical_file:
ical = (Calendar.from_ical(ical_file.read()))
self.icalendars += ical
def load_from_file(self, filepath):
"""Input a string or list of strings containing valid iCalendar filepaths
example: 'path1' (single file) OR ['path1', 'path2'] (multiple files)
returns a list of iCalendars as string (raw)
"""
if isinstance(filepath, list):
for path in filepath:
with open(path, mode='r') as ical_file:
ical = (Calendar.from_ical(ical_file.read()))
self.icalendars += ical
elif isinstance(filepath, str):
with open(filepath, mode='r') as ical_file:
ical = (Calendar.from_ical(ical_file.read()))
self.icalendars += ical
else:
raise Exception(f"Input: '{filepath}' is not a string or list!")
elif isinstance(filepath, str):
with open(filepath, mode='r') as ical_file:
ical = (Calendar.from_ical(ical_file.read()))
self.icalendars += ical
else:
raise Exception (f"Input: '{filepath}' is not a string or list!")
logger.info('loaded iCalendars from filepaths')
logger.info('loaded iCalendars from filepaths')
def get_events(self, timeline_start, timeline_end, timezone=None):
"""Input an arrow (time) object for:
* the beginning of timeline (events have to end after this time)
* the end of the timeline (events have to begin before this time)
* timezone if events should be formatted to local time
Returns a list of events sorted by date
"""
if type(timeline_start) == arrow.arrow.Arrow:
if timezone == None:
timezone = 'UTC'
t_start = timeline_start
t_end = timeline_end
else:
raise Exception('Please input a valid arrow (time) object!')
def get_events(self, timeline_start, timeline_end, timezone=None):
"""Input an arrow (time) object for:
* the beginning of timeline (events have to end after this time)
* the end of the timeline (events have to begin before this time)
* timezone if events should be formatted to local time
Returns a list of events sorted by date
"""
if type(timeline_start) == arrow.arrow.Arrow:
if timezone == None:
timezone = 'UTC'
t_start = timeline_start
t_end = timeline_end
else:
raise Exception('Please input a valid arrow (time) object!')
# parse non-recurring events
# parse non-recurring events
# Recurring events time-span has to be in this format:
# "%Y%m%dT%H%M%SZ" (python strftime)
fmt = lambda date: (date.year, date.month, date.day, date.hour,
date.minute, date.second)
# Recurring events time-span has to be in this format:
# "%Y%m%dT%H%M%SZ" (python strftime)
fmt = lambda date: (date.year, date.month, date.day, date.hour,
date.minute, date.second)
t_start_recurring = fmt(t_start)
t_end_recurring = fmt(t_end)
t_start_recurring = fmt(t_start)
t_end_recurring = fmt(t_end)
# Fetch recurring events
recurring_events = (recurring_ical_events.of(ical).between(
t_start_recurring, t_end_recurring)
for ical in self.icalendars)
# Fetch recurring events
recurring_events = (recurring_ical_events.of(ical).between(
t_start_recurring, t_end_recurring)
for ical in self.icalendars)
events = (
{
'title': events.get('SUMMARY').lstrip(),
events = (
{
'title': events.get('SUMMARY').lstrip(),
'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')
else arrow.get(events.get('DTSTART').dt).replace(tzinfo=timezone),
'begin': arrow.get(events.get('DTSTART').dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')
else arrow.get(events.get('DTSTART').dt).replace(tzinfo=timezone),
'end': arrow.get(events.get("DTEND").dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')
else arrow.get(events.get('DTEND').dt).replace(tzinfo=timezone)
'end':arrow.get(events.get("DTEND").dt).to(timezone) if (
arrow.get(events.get('dtstart').dt).format('HH:mm') != '00:00')
else arrow.get(events.get('DTEND').dt).replace(tzinfo=timezone)
} for ical in recurring_events for events in ical)
} for ical in recurring_events for events in ical)
# if any recurring events were found, add them to parsed_events
if events: self.parsed_events += list(events)
# Sort events by their beginning date
self.sort()
# if any recurring events were found, add them to parsed_events
if events: self.parsed_events += list(events)
return self.parsed_events
# Sort events by their beginning date
self.sort()
def sort(self):
"""Sort all parsed events in order of beginning time"""
if not self.parsed_events:
logger.debug('no events found to be sorted')
else:
# sort events by date
by_date = lambda event: event['begin']
self.parsed_events.sort(key=by_date)
return self.parsed_events
def clear_events(self):
"""clear previously parsed events"""
def sort(self):
"""Sort all parsed events in order of beginning time"""
if not self.parsed_events:
logger.debug('no events found to be sorted')
else:
# sort events by date
by_date = lambda event: event['begin']
self.parsed_events.sort(key=by_date)
self.parsed_events = []
@staticmethod
def all_day(event):
"""Check if an event is an all day event.
Returns True if event is all day, else False
"""
if not ('end' and 'begin') in event:
print('Events must have a starting and ending time')
raise Exception('This event is not valid!')
else:
begin, end = event['begin'], event['end']
duration = end - begin
if (begin.format('HH:mm') == '00:00' and end.format('HH:mm') == '00:00'
and duration.days >= 1):
return True
else:
return False
def clear_events(self):
"""clear previously parsed events"""
@staticmethod
def get_system_tz():
"""Get the timezone set by the system"""
self.parsed_events = []
try:
local_tz = time.tzname[1]
except:
print('System timezone could not be parsed!')
print('Please set timezone manually!. Setting timezone to None...')
local_tz = None
return local_tz
@staticmethod
def all_day(event):
"""Check if an event is an all day event.
Returns True if event is all day, else False
"""
if not ('end' and 'begin') in event:
print('Events must have a starting and ending time')
raise Exception('This event is not valid!')
else:
begin, end = event['begin'], event['end']
duration = end - begin
if (begin.format('HH:mm') == '00:00' and end.format('HH:mm') == '00:00'
and duration.days >= 1):
return True
else:
return False
def show_events(self, fmt='DD MMM YY HH:mm'):
"""print all parsed events in a more readable way
use the format (fmt) parameter to specify the date format
see https://arrow.readthedocs.io/en/latest/#supported-tokens
for more info tokens
"""
@staticmethod
def get_system_tz():
"""Get the timezone set by the system"""
try:
local_tz = time.tzname[1]
except:
print('System timezone could not be parsed!')
print('Please set timezone manually!. Setting timezone to None...')
local_tz = None
return local_tz
def show_events(self, fmt='DD MMM YY HH:mm'):
"""print all parsed events in a more readable way
use the format (fmt) parameter to specify the date format
see https://arrow.readthedocs.io/en/latest/#supported-tokens
for more info tokens
"""
if not self.parsed_events:
logger.debug('no events found to be shown')
else:
line_width = max(len(_['title']) for _ in self.parsed_events)
for events in self.parsed_events:
title = events['title']
begin, end = events['begin'].format(fmt), events['end'].format(fmt)
print('{0} {1} | {2} | {3}'.format(
title, ' ' * (line_width - len(title)), begin, end))
if not self.parsed_events:
logger.debug('no events found to be shown')
else:
line_width = max(len(_['title']) for _ in self.parsed_events)
for events in self.parsed_events:
title = events['title']
begin, end = events['begin'].format(fmt), events['end'].format(fmt)
print('{0} {1} | {2} | {3}'.format(
title, ' ' * (line_width - len(title)), begin, end))
if __name__ == '__main__':
print(f'running {filename} in standalone mode')
print(f'running {filename} in standalone mode')

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Custom image class for Inkycal Project
@@ -18,317 +18,316 @@ import logging
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Inkyimage:
"""Custom Imge class written for commonly used image operations.
"""
def __init__(self, image=None):
"""Initialize Inkyimage module"""
# no image initially
self.image = image
# give an OK message
logger.info(f'{filename} loaded')
def load(self, path):
"""loads an image from a URL or filepath.
Args:
- path:The full path or url of the image file
e.g. `https://sample.com/logo.png` or `/home/pi/Downloads/nice_pic.png`
Raises:
- FileNotFoundError: This Exception is raised when the file could not be
found.
- OSError: A OSError is raised when the URL doesn't point to the correct
file-format, i.e. is not an image
- TypeError: if the URLS doesn't start with htpp
"""Custom Imge class written for commonly used image operations.
"""
# Try to open the image if it exists and is an image file
try:
if path.startswith('http'):
logger.info('loading image from URL')
image = Image.open(requests.get(path, stream=True).raw)
else:
logger.info('loading image from local path')
image = Image.open(path)
except FileNotFoundError:
logger.error('No image file found', exc_info=True)
raise Exception('Your file could not be found. Please check the filepath')
except OSError:
logger.error('Invalid Image file provided', exc_info=True)
raise Exception('Please check if the path points to an image file.')
logger.info(f'width: {image.width}, height: {image.height}')
def __init__(self, image=None):
"""Initialize Inkyimage module"""
image.convert(mode='RGBA') #convert to a more suitable format
self.image = image
logger.info('loaded Image')
def clear(self):
"""Removes currently saved image if present."""
if self.image:
self.image = None
logger.info('cleared previous image')
def _preview(self):
"""Preview the image on gpicview (only works on Rapsbian with Desktop)"""
if self._image_loaded():
path = '/home/pi/Desktop/'
self.image.save(path+'temp.png')
os.system("gpicview "+path+'temp.png')
os.system('rm '+path+'temp.png')
@staticmethod
def preview(image):
""""Previews an image on gpicview (only works on Rapsbian with Desktop).
"""
path = '/home/pi/Desktop/'
image.save(path+'temp.png')
os.system("gpicview "+path+'temp.png')
os.system('rm '+path+'temp.png')
def _image_loaded(self):
"""returns True if image was loaded"""
if self.image:
return True
else:
logger.error('image not loaded')
return False
def flip(self, angle):
"""Flips the image by the given angle.
Args:
- angle:->int. A multiple of 90, e.g. 90, 180, 270, 360.
"""
if self._image_loaded():
image = self.image
if not angle % 90 == 0:
logger.error('Angle must be a multiple of 90')
return
image = image.rotate(angle, expand = True)
self.image = image
logger.info(f'flipped image by {angle} degrees')
def autoflip(self, layout):
"""flips the image automatically to the given layout.
Args:
- layout:-> str. Choose `horizontal` or `vertical`.
Checks the image's width and height.
In horizontal mode, the image is flipped if the image height is greater
than the image width.
In vertical mode, the image is flipped if the image width is greater
than the image height.
"""
if self._image_loaded():
image = self.image
if layout == 'horizontal':
if (image.height > image.width):
logger.info('image width greater than image height, flipping')
image = image.rotate(90, expand=True)
elif layout == 'vertical':
if (image.width > image.height):
logger.info('image width greater than image height, flipping')
image = image.rotate(90, expand=True)
else:
logger.error('layout not supported')
return
self.image = image
def remove_alpha(self):
"""Removes transparency if image has transparency.
Checks if an image has an alpha band and replaces the transparency with
white pixels.
"""
if self._image_loaded():
image = self.image
if len(image.getbands()) == 4:
logger.info('removing alpha channel')
bg = Image.new('RGBA', (image.width, image.height), 'white')
im = Image.alpha_composite(bg, image)
self.image.paste(im, (0,0))
logger.info('removed transparency')
def resize(self, width=None, height=None):
"""Resize an image to desired width or height"""
if self._image_loaded():
if width == None and height == None:
logger.error('no height of width specified')
return
image = self.image
if width:
initial_width = image.width
wpercent = (width/float(image.width))
hsize = int((float(image.height)*float(wpercent)))
image = image.resize((width, hsize), Image.ANTIALIAS)
logger.info(f"resized image from {initial_width} to {image.width}")
# no image initially
self.image = image
if height:
initial_height = image.height
hpercent = (height / float(image.height))
wsize = int(float(image.width) * float(hpercent))
image = image.resize((wsize, height), Image.ANTIALIAS)
logger.info(f"resized image from {initial_height} to {image.height}")
# give an OK message
logger.info(f'{filename} loaded')
def load(self, path):
"""loads an image from a URL or filepath.
Args:
- path:The full path or url of the image file
e.g. `https://sample.com/logo.png` or `/home/pi/Downloads/nice_pic.png`
Raises:
- FileNotFoundError: This Exception is raised when the file could not be
found.
- OSError: A OSError is raised when the URL doesn't point to the correct
file-format, i.e. is not an image
- TypeError: if the URLS doesn't start with htpp
"""
# Try to open the image if it exists and is an image file
try:
if path.startswith('http'):
logger.info('loading image from URL')
image = Image.open(requests.get(path, stream=True).raw)
else:
logger.info('loading image from local path')
image = Image.open(path)
except FileNotFoundError:
logger.error('No image file found', exc_info=True)
raise Exception('Your file could not be found. Please check the filepath')
except OSError:
logger.error('Invalid Image file provided', exc_info=True)
raise Exception('Please check if the path points to an image file.')
logger.info(f'width: {image.width}, height: {image.height}')
image.convert(mode='RGBA') # convert to a more suitable format
self.image = image
logger.info('loaded Image')
@staticmethod
def merge(image1, image2):
"""Merges two images into one.
def clear(self):
"""Removes currently saved image if present."""
if self.image:
self.image = None
logger.info('cleared previous image')
Replaces white pixels of the first image with transparent ones. Then pastes
the first image on the second one.
def _preview(self):
"""Preview the image on gpicview (only works on Rapsbian with Desktop)"""
if self._image_loaded():
path = '/home/pi/Desktop/'
self.image.save(path + 'temp.png')
os.system("gpicview " + path + 'temp.png')
os.system('rm ' + path + 'temp.png')
Args:
- image1: A PIL Image object in 'RGBA' mode.
- image2: A PIL Image object in 'RGBA' mode.
@staticmethod
def preview(image):
""""Previews an image on gpicview (only works on Rapsbian with Desktop).
"""
path = '/home/pi/Desktop/'
image.save(path + 'temp.png')
os.system("gpicview " + path + 'temp.png')
os.system('rm ' + path + 'temp.png')
Returns:
- A single image.
"""
def _image_loaded(self):
"""returns True if image was loaded"""
if self.image:
return True
else:
logger.error('image not loaded')
return False
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)
def flip(self, angle):
"""Flips the image by the given angle.
image2 = clear_white(image2)
image1.paste(image2, (0,0), image2)
logger.info('merged given images into one')
Args:
- angle:->int. A multiple of 90, e.g. 90, 180, 270, 360.
"""
if self._image_loaded():
return image1
image = self.image
if not angle % 90 == 0:
logger.error('Angle must be a multiple of 90')
return
image = image.rotate(angle, expand=True)
self.image = image
logger.info(f'flipped image by {angle} degrees')
def to_palette(self, palette, dither=True):
"""Maps an image to a given colour palette.
def autoflip(self, layout):
"""flips the image automatically to the given layout.
Maps each pixel from the image to a colour from the palette.
Args:
- layout:-> str. Choose `horizontal` or `vertical`.
Args:
- palette: A supported token. (see below)
- dither:->bool. Use dithering? Set to `False` for solid colour fills.
Checks the image's width and height.
Returns:
- two images: one for the coloured band and one for the black band.
In horizontal mode, the image is flipped if the image height is greater
than the image width.
Raises:
- ValueError if palette token is not supported
In vertical mode, the image is flipped if the image width is greater
than the image height.
"""
if self._image_loaded():
Supported palette tokens:
image = self.image
if layout == 'horizontal':
if (image.height > image.width):
logger.info('image width greater than image height, flipping')
image = image.rotate(90, expand=True)
>>> 'bwr' # black-white-red
>>> 'bwy' # black-white-yellow
>>> 'bw' # black-white
"""
# Check if an image is loaded
if self._image_loaded():
image = self.image.convert('RGB')
else:
logger.error('No image loaded')
elif layout == 'vertical':
if (image.width > image.height):
logger.info('image width greater than image height, flipping')
image = image.rotate(90, expand=True)
else:
logger.error('layout not supported')
return
self.image = image
if palette == 'bwr':
# black-white-red palette
pal = [255,255,255, 0,0,0, 255,0,0]
def remove_alpha(self):
"""Removes transparency if image has transparency.
elif palette == 'bwy':
# black-white-yellow palette
pal = [255,255,255, 0,0,0, 255,255,0]
Checks if an image has an alpha band and replaces the transparency with
white pixels.
"""
if self._image_loaded():
image = self.image
elif palette == 'bw':
pal = None
if len(image.getbands()) == 4:
logger.info('removing alpha channel')
bg = Image.new('RGBA', (image.width, image.height), 'white')
im = Image.alpha_composite(bg, image)
else:
logger.error('The given palette is unsupported.')
raise ValueError('The given palette is not supported.')
self.image.paste(im, (0, 0))
logger.info('removed transparency')
if pal:
# The palette needs to have 256 colors, for this, the black-colour
# is added until the
colours = len(pal) // 3
#print(f'The palette has {colours} colours')
def resize(self, width=None, height=None):
"""Resize an image to desired width or height"""
if self._image_loaded():
if 256 % colours != 0:
#print('Filling palette with black')
pal += (256 % colours) * [0,0,0]
if width == None and height == None:
logger.error('no height of width specified')
return
#print(pal)
colours = len(pal) // 3
#print(f'The palette now has {colours} colours')
image = self.image
# Create a dummy image to be used as a palette
palette_im = Image.new('P', (1,1))
if width:
initial_width = image.width
wpercent = (width / float(image.width))
hsize = int((float(image.height) * float(wpercent)))
image = image.resize((width, hsize), Image.ANTIALIAS)
logger.info(f"resized image from {initial_width} to {image.width}")
self.image = image
# Attach the created palette. The palette should have 256 colours
# equivalent to 768 integers
palette_im.putpalette(pal* (256//colours))
if height:
initial_height = image.height
hpercent = (height / float(image.height))
wsize = int(float(image.width) * float(hpercent))
image = image.resize((wsize, height), Image.ANTIALIAS)
logger.info(f"resized image from {initial_height} to {image.height}")
self.image = image
# Quantize the image to given palette
quantized_im = image.quantize(palette=palette_im, dither=dither)
quantized_im = quantized_im.convert('RGB')
@staticmethod
def merge(image1, image2):
"""Merges two images into one.
# get rgb of the non-black-white colour from the palette
rgb = [pal[x:x+3] for x in range(0, len(pal),3)]
rgb = [col for col in rgb if col != [0,0,0] and col != [255,255,255]][0]
r_col, g_col, b_col = rgb
#print(f'r:{r_col} g:{g_col} b:{b_col}')
Replaces white pixels of the first image with transparent ones. Then pastes
the first image on the second one.
# Create an image buffer for black pixels
buffer1 = numpy.array(quantized_im)
Args:
- image1: A PIL Image object in 'RGBA' mode.
- image2: A PIL Image object in 'RGBA' mode.
# Get RGB values of each pixel
r,g,b = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2]
Returns:
- A single image.
"""
# convert coloured pixels to white
buffer1[numpy.logical_and(r==r_col, g==g_col)] = [255,255,255]
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)
# reconstruct image for black-band
im_black = Image.fromarray(buffer1)
image2 = clear_white(image2)
image1.paste(image2, (0, 0), image2)
logger.info('merged given images into one')
# Create a buffer for coloured pixels
buffer2 = numpy.array(quantized_im)
return image1
# Get RGB values of each pixel
r,g,b = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2]
def to_palette(self, palette, dither=True):
"""Maps an image to a given colour palette.
# convert black pixels to white
buffer2[numpy.logical_and(r==0, g==0)] = [255,255,255]
Maps each pixel from the image to a colour from the palette.
# convert non-white pixels to black
buffer2[numpy.logical_and(g==g_col, b==0)] = [0,0,0]
Args:
- palette: A supported token. (see below)
- dither:->bool. Use dithering? Set to `False` for solid colour fills.
# reconstruct image for colour-band
im_colour = Image.fromarray(buffer2)
Returns:
- two images: one for the coloured band and one for the black band.
#self.preview(im_black)
#self.preview(im_colour)
Raises:
- ValueError if palette token is not supported
else:
im_black = image.convert('1', dither=dither)
im_colour = Image.new(mode='RGB', size=im_black.size, color='white')
Supported palette tokens:
logger.info('mapped image to specified palette')
>>> 'bwr' # black-white-red
>>> 'bwy' # black-white-yellow
>>> 'bw' # black-white
"""
# Check if an image is loaded
if self._image_loaded():
image = self.image.convert('RGB')
else:
logger.error('No image loaded')
return im_black, im_colour
if palette == 'bwr':
# black-white-red palette
pal = [255, 255, 255, 0, 0, 0, 255, 0, 0]
elif palette == 'bwy':
# black-white-yellow palette
pal = [255, 255, 255, 0, 0, 0, 255, 255, 0]
elif palette == 'bw':
pal = None
else:
logger.error('The given palette is unsupported.')
raise ValueError('The given palette is not supported.')
if pal:
# The palette needs to have 256 colors, for this, the black-colour
# is added until the
colours = len(pal) // 3
# print(f'The palette has {colours} colours')
if 256 % colours != 0:
# print('Filling palette with black')
pal += (256 % colours) * [0, 0, 0]
# print(pal)
colours = len(pal) // 3
# print(f'The palette now has {colours} colours')
# Create a dummy image to be used as a palette
palette_im = Image.new('P', (1, 1))
# Attach the created palette. The palette should have 256 colours
# equivalent to 768 integers
palette_im.putpalette(pal * (256 // colours))
# Quantize the image to given palette
quantized_im = image.quantize(palette=palette_im, dither=dither)
quantized_im = quantized_im.convert('RGB')
# get rgb of the non-black-white colour from the palette
rgb = [pal[x:x + 3] for x in range(0, len(pal), 3)]
rgb = [col for col in rgb if col != [0, 0, 0] and col != [255, 255, 255]][0]
r_col, g_col, b_col = rgb
# print(f'r:{r_col} g:{g_col} b:{b_col}')
# Create an image buffer for black pixels
buffer1 = numpy.array(quantized_im)
# Get RGB values of each pixel
r, g, b = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2]
# convert coloured pixels to white
buffer1[numpy.logical_and(r == r_col, g == g_col)] = [255, 255, 255]
# reconstruct image for black-band
im_black = Image.fromarray(buffer1)
# Create a buffer for coloured pixels
buffer2 = numpy.array(quantized_im)
# Get RGB values of each pixel
r, g, b = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2]
# convert black pixels to white
buffer2[numpy.logical_and(r == 0, g == 0)] = [255, 255, 255]
# convert non-white pixels to black
buffer2[numpy.logical_and(g == g_col, b == 0)] = [0, 0, 0]
# reconstruct image for colour-band
im_colour = Image.fromarray(buffer2)
# self.preview(im_black)
# self.preview(im_colour)
else:
im_black = image.convert('1', dither=dither)
im_colour = Image.new(mode='RGB', size=im_black.size, color='white')
logger.info('mapped image to specified palette')
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Agenda module for Inky-Calendar Project
Copyright by aceisace
@@ -15,219 +15,219 @@ import arrow
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Agenda(inkycal_module):
"""Agenda class
Create agenda and show events from given icalendars
"""
"""Agenda class
Create agenda and show events from given icalendars
"""
name = "Agenda - Display upcoming events from given iCalendars"
name = "Agenda - Display upcoming events from given iCalendars"
requires = {
"ical_urls" : {
"label":"iCalendar URL/s, separate multiple ones with a comma",
},
requires = {
"ical_urls": {
"label": "iCalendar URL/s, separate multiple ones with a comma",
},
}
optional = {
"ical_files" : {
"label":"iCalendar filepaths, separated with a comma",
},
optional = {
"ical_files": {
"label": "iCalendar filepaths, separated with a comma",
},
"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",
"default":"HH:mm",
},
"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",
"default": "HH:mm",
},
}
def __init__(self, config):
"""Initialize inkycal_agenda module"""
def __init__(self, config):
"""Initialize inkycal_agenda module"""
super().__init__(config)
super().__init__(config)
config = config['config']
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
logger.exception(f'config is missing "{param}"')
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
logger.exception(f'config is missing "{param}"')
# module specific parameters
self.date_format = config['date_format']
self.time_format = config['time_format']
self.language = config['language']
# module specific parameters
self.date_format = config['date_format']
self.time_format = config['time_format']
self.language = config['language']
# Check if ical_files is an empty string
if config['ical_urls'] and isinstance(config['ical_urls'], str):
self.ical_urls = config['ical_urls'].split(',')
else:
self.ical_urls = config['ical_urls']
# Check if ical_files is an empty string
if config['ical_urls'] and isinstance(config['ical_urls'], str):
self.ical_urls = config['ical_urls'].split(',')
else:
self.ical_urls = config['ical_urls']
# Check if ical_files is an empty string
if config['ical_files'] and isinstance(config['ical_files'], str):
self.ical_files = config['ical_files'].split(',')
else:
self.ical_files = config['ical_files']
# Check if ical_files is an empty string
if config['ical_files'] and isinstance(config['ical_files'], str):
self.ical_files = config['ical_files'].split(',')
else:
self.ical_files = config['ical_files']
# Additional config
self.timezone = get_system_tz()
# Additional config
self.timezone = get_system_tz()
# give an OK message
print(f'{filename} loaded')
# give an OK message
print(f'{filename} loaded')
def generate_image(self):
"""Generate image for this module"""
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
logger.info(f'Image size: {im_size}')
# 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')
# 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')
# Calculate the max number of lines that can fit on the image
line_spacing = 1
line_height = int(self.font.getsize('hg')[1]) + line_spacing
line_width = im_width
max_lines = im_height // line_height
logger.debug(f'max lines: {max_lines}')
# Calculate the max number of lines that can fit on the image
line_spacing = 1
line_height = int(self.font.getsize('hg')[1]) + line_spacing
line_width = im_width
max_lines = im_height // line_height
logger.debug(f'max lines: {max_lines}')
# Create timeline for agenda
now = arrow.now()
today = now.floor('day')
# Create timeline for agenda
now = arrow.now()
today = now.floor('day')
# Create a list of dates for the next days
agenda_events = [
{'begin':today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format,locale=self.language)}
for _ in range(max_lines)]
# Create a list of dates for the next days
agenda_events = [
{'begin': today.shift(days=+_),
'title': today.shift(days=+_).format(
self.date_format, locale=self.language)}
for _ in range(max_lines)]
# Load icalendar from config
self.ical = iCalendar()
parser = self.ical
# Load icalendar from config
self.ical = iCalendar()
parser = self.ical
if self.ical_urls:
parser.load_url(self.ical_urls)
if self.ical_urls:
parser.load_url(self.ical_urls)
if self.ical_files:
parser.load_from_file(self.ical_files)
if self.ical_files:
parser.load_from_file(self.ical_files)
# Load events from all icalendar in timerange
upcoming_events = parser.get_events(today, agenda_events[-1]['begin'],
self.timezone)
# Load events from all icalendar in timerange
upcoming_events = parser.get_events(today, agenda_events[-1]['begin'],
self.timezone)
# Sort events by beginning time
parser.sort()
#parser.show_events()
# Sort events by beginning time
parser.sort()
# parser.show_events()
# Set the width for date, time and event titles
date_width = int(max([self.font.getsize(
dates['begin'].format(self.date_format, locale=self.language))[0]
for dates in agenda_events]) * 1.2)
logger.debug(f'date_width: {date_width}')
# Set the width for date, time and event titles
date_width = int(max([self.font.getsize(
dates['begin'].format(self.date_format, locale=self.language))[0]
for dates in agenda_events]) * 1.2)
logger.debug(f'date_width: {date_width}')
# Calculate positions for each line
line_pos = [(0, int(line * line_height)) for line in range(max_lines)]
logger.debug(f'line_pos: {line_pos}')
# Calculate positions for each line
line_pos = [(0, int(line * line_height)) for line in range(max_lines)]
logger.debug(f'line_pos: {line_pos}')
# Check if any events were filtered
if upcoming_events:
logger.info('Managed to parse events from urls')
# Check if any events were filtered
if upcoming_events:
logger.info('Managed to parse events from urls')
# Find out how much space the event times take
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=self.language))[0]
for events in upcoming_events]) * 1.2)
logger.debug(f'time_width: {time_width}')
# Find out how much space the event times take
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=self.language))[0]
for events in upcoming_events]) * 1.2)
logger.debug(f'time_width: {time_width}')
# Calculate x-pos for time
x_time = date_width
logger.debug(f'x-time: {x_time}')
# Calculate x-pos for time
x_time = date_width
logger.debug(f'x-time: {x_time}')
# Find out how much space is left for event titles
event_width = im_width - time_width - date_width
logger.debug(f'width for events: {event_width}')
# Find out how much space is left for event titles
event_width = im_width - time_width - date_width
logger.debug(f'width for events: {event_width}')
# Calculate x-pos for event titles
x_event = date_width + time_width
logger.debug(f'x-event: {x_event}')
# Calculate x-pos for event titles
x_event = date_width + time_width
logger.debug(f'x-event: {x_event}')
# Merge list of dates and list of events
agenda_events += upcoming_events
# Merge list of dates and list of events
agenda_events += upcoming_events
# Sort the combined list in chronological order of dates
by_date = lambda event: event['begin']
agenda_events.sort(key = by_date)
# Sort the combined list in chronological order of dates
by_date = lambda event: event['begin']
agenda_events.sort(key=by_date)
# Delete more entries than can be displayed (max lines)
del agenda_events[max_lines:]
# Delete more entries than can be displayed (max lines)
del agenda_events[max_lines:]
self._agenda_events = agenda_events
self._agenda_events = agenda_events
cursor = 0
for _ in agenda_events:
title = _['title']
cursor = 0
for _ in agenda_events:
title = _['title']
# Check if item is a date
if not 'end' in _:
ImageDraw.Draw(im_colour).line(
(0, line_pos[cursor][1], im_width, line_pos[cursor][1]),
fill = 'black')
# Check if item is a date
if not 'end' in _:
ImageDraw.Draw(im_colour).line(
(0, line_pos[cursor][1], im_width, line_pos[cursor][1]),
fill='black')
write(im_black, line_pos[cursor], (date_width, line_height),
title, font = self.font, alignment='left')
write(im_black, line_pos[cursor], (date_width, line_height),
title, font=self.font, alignment='left')
cursor += 1
cursor += 1
# Check if item is an event
if 'end' in _:
time = _['begin'].format(self.time_format)
# Check if item is an event
if 'end' in _:
time = _['begin'].format(self.time_format)
# Check if event is all day, if not, add the time
if parser.all_day(_) == False:
write(im_black, (x_time, line_pos[cursor][1]),
(time_width, line_height), time,
font = self.font, alignment='left')
# Check if event is all day, if not, add the time
if parser.all_day(_) == False:
write(im_black, (x_time, line_pos[cursor][1]),
(time_width, line_height), time,
font=self.font, alignment='left')
write(im_black, (x_event, line_pos[cursor][1]),
(event_width, line_height),
''+title, font = self.font, alignment='left')
cursor += 1
write(im_black, (x_event, line_pos[cursor][1]),
(event_width, line_height),
'' + title, font=self.font, alignment='left')
cursor += 1
# If no events were found, write only dates and lines
else:
logger.info('no events found')
# If no events were found, write only dates and lines
else:
logger.info('no events found')
cursor = 0
for _ in agenda_events:
title = _['title']
ImageDraw.Draw(im_colour).line(
(0, line_pos[cursor][1], im_width, line_pos[cursor][1]),
fill = 'black')
cursor = 0
for _ in agenda_events:
title = _['title']
ImageDraw.Draw(im_colour).line(
(0, line_pos[cursor][1], im_width, line_pos[cursor][1]),
fill='black')
write(im_black, line_pos[cursor], (date_width, line_height),
title, font = self.font, alignment='left')
write(im_black, line_pos[cursor], (date_width, line_height),
title, font=self.font, alignment='left')
cursor += 1
cursor += 1
# return the images ready for the display
return im_black, im_colour
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone mode')
print(f'running {filename} in standalone mode')

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Calendar module for Inky-Calendar Project
Copyright by aceisace
@@ -15,332 +15,330 @@ logger = logging.getLogger(filename)
class Calendar(inkycal_module):
"""Calendar class
Create monthly calendar and show events from given icalendars
"""
"""Calendar class
Create monthly calendar and show events from given icalendars
"""
name = "Calendar - Show monthly calendar with events from iCalendars"
name = "Calendar - Show monthly calendar with events from iCalendars"
optional = {
optional = {
"week_starts_on" : {
"label":"When does your week start? (default=Monday)",
"options": ["Monday", "Sunday"],
"default": "Monday"
},
"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
},
"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",
},
"ical_urls": {
"label": "iCalendar URL/s, separate multiple ones with a comma",
},
"ical_files" : {
"label":"iCalendar filepaths, separated with a comma",
},
"ical_files": {
"label": "iCalendar filepaths, separated with a comma",
},
"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",
},
"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"
},
"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, config):
"""Initialize inkycal_calendar module"""
def __init__(self, config):
"""Initialize inkycal_calendar module"""
super().__init__(config)
config = config['config']
super().__init__(config)
config = config['config']
# optional parameters
self.weekstart = config['week_starts_on']
self.show_events = config['show_events']
self.date_format = config["date_format"]
self.time_format = config['time_format']
self.language = config['language']
# optional parameters
self.weekstart = config['week_starts_on']
self.show_events = config['show_events']
self.date_format = config["date_format"]
self.time_format = config['time_format']
self.language = config['language']
if config['ical_urls'] and isinstance(config['ical_urls'], str):
self.ical_urls = config['ical_urls'].split(',')
else:
self.ical_urls = config['ical_urls']
if config['ical_urls'] and isinstance(config['ical_urls'], str):
self.ical_urls = config['ical_urls'].split(',')
else:
self.ical_urls = config['ical_urls']
if config['ical_files'] and isinstance(config['ical_files'], str):
self.ical_files = config['ical_files'].split(',')
else:
self.ical_files = config['ical_files']
if config['ical_files'] and isinstance(config['ical_files'], str):
self.ical_files = config['ical_files'].split(',')
else:
self.ical_files = config['ical_files']
# additional configuration
self.timezone = get_system_tz()
self.num_font = ImageFont.truetype(
fonts['NotoSans-SemiCondensed'], size = self.fontsize)
# additional configuration
self.timezone = get_system_tz()
self.num_font = ImageFont.truetype(
fonts['NotoSans-SemiCondensed'], size=self.fontsize)
# give an OK message
print(f'{filename} loaded')
# give an OK message
print(f'{filename} loaded')
def generate_image(self):
"""Generate image for this module"""
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
logger.info(f'Image size: {im_size}')
# 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')
# 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')
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
weekdays_height = int(self.font.getsize('hg')[1] * 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
# Allocate space for month-names, weekdays etc.
month_name_height = int(im_height * 0.10)
weekdays_height = int(self.font.getsize('hg')[1] * 1.25)
logger.debug(f"month_name_height: {month_name_height}")
logger.debug(f"weekdays_height: {weekdays_height}")
if self.show_events == True:
logger.debug("Allocating space for events")
calendar_height = int(im_height * 0.6)
events_height = im_height - month_name_height - weekdays_height - calendar_height
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
logger.debug(f'events-section size: {im_width} x {events_height} px')
else:
logger.debug("Not allocating space for events")
calendar_height = im_height - month_name_height - weekdays_height
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
if self.show_events == True:
logger.debug("Allocating space for events")
calendar_height = int(im_height * 0.6)
events_height = im_height - month_name_height - weekdays_height - calendar_height
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
logger.debug(f'events-section size: {im_width} x {events_height} px')
else:
logger.debug("Not allocating space for events")
calendar_height = im_height - month_name_height - weekdays_height
logger.debug(f'calendar-section size: {im_width} x {calendar_height} px')
# Create a 7x6 grid and calculate icon sizes
calendar_rows, calendar_cols = 6, 7
icon_width = im_width // calendar_cols
icon_height = calendar_height // calendar_rows
logger.debug(f"icon_size: {icon_width}x{icon_height}px")
# Create a 7x6 grid and calculate icon sizes
calendar_rows, calendar_cols = 6, 7
icon_width = im_width // calendar_cols
icon_height = calendar_height // calendar_rows
logger.debug(f"icon_size: {icon_width}x{icon_height}px")
# Calculate spacings for calendar area
x_spacing_calendar = int((im_width % calendar_cols) / 2)
y_spacing_calendar = int((im_height % calendar_rows) / 2)
# Calculate spacings for calendar area
x_spacing_calendar = int((im_width % calendar_cols) / 2)
y_spacing_calendar = int((im_height % calendar_rows) / 2)
logger.debug(f"x_spacing_calendar: {x_spacing_calendar}")
logger.debug(f"y_spacing_calendar :{y_spacing_calendar}")
logger.debug(f"x_spacing_calendar: {x_spacing_calendar}")
logger.debug(f"y_spacing_calendar :{y_spacing_calendar}")
# Calculate positions for days of month
grid_start_y = (month_name_height + weekdays_height + y_spacing_calendar)
grid_start_x = x_spacing_calendar
# Calculate positions for days of month
grid_start_y = (month_name_height + weekdays_height + y_spacing_calendar)
grid_start_x = x_spacing_calendar
grid_coordinates = [(grid_start_x + icon_width*x, grid_start_y + icon_height*y)
for y in range(calendar_rows) for x in range(calendar_cols)]
grid_coordinates = [(grid_start_x + icon_width * x, grid_start_y + icon_height * y)
for y in range(calendar_rows) for x in range(calendar_cols)]
weekday_pos = [(grid_start_x + icon_width*_, month_name_height) for _ in
range(calendar_cols)]
weekday_pos = [(grid_start_x + icon_width * _, month_name_height) for _ in
range(calendar_cols)]
now = arrow.now(tz = self.timezone)
now = arrow.now(tz=self.timezone)
# Set weekstart of calendar to specified weekstart
if self.weekstart == "Monday":
cal.setfirstweekday(cal.MONDAY)
weekstart = now.shift(days = - now.weekday())
else:
cal.setfirstweekday(cal.SUNDAY)
weekstart = now.shift(days = - now.isoweekday())
# Set weekstart of calendar to specified weekstart
if self.weekstart == "Monday":
cal.setfirstweekday(cal.MONDAY)
weekstart = now.shift(days=- now.weekday())
else:
cal.setfirstweekday(cal.SUNDAY)
weekstart = now.shift(days=- now.isoweekday())
# Write the name of current month
write(im_black, (0,0),(im_width, month_name_height),
str(now.format('MMMM',locale=self.language)), font = self.font,
autofit = True)
# Write the name of current month
write(im_black, (0, 0), (im_width, month_name_height),
str(now.format('MMMM', locale=self.language)), font=self.font,
autofit=True)
# Set up weeknames in local language and add to main section
weekday_names = [weekstart.shift(days=+_).format('ddd',locale=self.language)
for _ in range(7)]
logger.debug(f'weekday names: {weekday_names}')
# Set up weeknames in local language and add to main section
weekday_names = [weekstart.shift(days=+_).format('ddd', locale=self.language)
for _ in range(7)]
logger.debug(f'weekday names: {weekday_names}')
for _ in range(len(weekday_pos)):
write(
im_black,
weekday_pos[_],
(icon_width, weekdays_height),
weekday_names[_],
font = self.font,
autofit = True,
fill_height=1.0
)
# Create a calendar template and flatten (remove nestings)
flatten = lambda z: [x for y in z for x in y]
calendar_flat = flatten(cal.monthcalendar(now.year, now.month))
#logger.debug(f" calendar_flat: {calendar_flat}")
# Map days of month to co-ordinates of grid -> 3: (row2_x,col3_y)
grid = {}
for i in calendar_flat:
if i != 0:
grid[i] = grid_coordinates[calendar_flat.index(i)]
#logger.debug(f"grid:{grid}")
# remove zeros from calendar since they are not required
calendar_flat = [num for num in calendar_flat if num != 0]
# Add the numbers on the correct positions
for number in calendar_flat:
if number != int(now.day):
write(im_black, grid[number], (icon_width, icon_height),
str(number), font = self.num_font, fill_height = 0.5, fill_width=0.5)
# Draw a red/black circle with the current day of month in white
icon = Image.new('RGBA', (icon_width, icon_height))
current_day_pos = grid[int(now.day)]
x_circle,y_circle = int(icon_width/2), int(icon_height/2)
radius = int(icon_width * 0.2)
ImageDraw.Draw(icon).ellipse(
(x_circle-radius, y_circle-radius, x_circle+radius, y_circle+radius),
fill= 'black', outline=None)
write(icon, (0,0), (icon_width, icon_height), str(now.day),
font=self.num_font, fill_height = 0.5, colour='white')
im_colour.paste(icon, current_day_pos, icon)
# If events should be loaded and shown...
if self.show_events == True:
# If this month requires 5 instead of 6 rows, increase event section height
if len(cal.monthcalendar(now.year, now.month)) == 5:
events_height += icon_height
# If this month requires 4 instead of 6 rows, increase event section height
elif len(cal.monthcalendar(now.year, now.month)) == 4:
events_height += icon_height * 2
# import the ical-parser
from inkycal.modules.ical_parser import iCalendar
# find out how many lines can fit at max in the event section
line_spacing = 0
max_event_lines = events_height // (self.font.getsize('hg')[1] +
line_spacing)
# generate list of coordinates for each line
events_offset = im_height - events_height
event_lines = [(0, events_offset + int(events_height/max_event_lines*_))
for _ in range(max_event_lines)]
#logger.debug(f"event_lines {event_lines}")
# timeline for filtering events within this month
month_start = arrow.get(now.floor('month'))
month_end = arrow.get(now.ceil('month'))
# fetch events from given icalendars
self.ical = iCalendar()
parser = self.ical
if self.ical_urls:
parser.load_url(self.ical_urls)
if self.ical_files:
parser.load_from_file(self.ical_files)
# Filter events for full month (even past ones) for drawing event icons
month_events = parser.get_events(month_start, month_end, self.timezone)
parser.sort()
self.month_events = month_events
# find out on which days of this month events are taking place
days_with_events = [int(events['begin'].format('D')) for events in
month_events]
# remove duplicates (more than one event in a single day)
list(set(days_with_events)).sort()
self._days_with_events = days_with_events
# Draw a border with specified parameters around days with events
for days in days_with_events:
if days in grid:
draw_border(
im_colour,
grid[days],
(icon_width, icon_height),
radius = 6,
thickness= 1,
shrinkage = (0.4, 0.2)
for _ in range(len(weekday_pos)):
write(
im_black,
weekday_pos[_],
(icon_width, weekdays_height),
weekday_names[_],
font=self.font,
autofit=True,
fill_height=1.0
)
# Filter upcoming events until 4 weeks in the future
parser.clear_events()
upcoming_events = parser.get_events(now, now.shift(weeks=4),
self.timezone)
self._upcoming_events = upcoming_events
# Create a calendar template and flatten (remove nestings)
flatten = lambda z: [x for y in z for x in y]
calendar_flat = flatten(cal.monthcalendar(now.year, now.month))
# logger.debug(f" calendar_flat: {calendar_flat}")
# delete events which won't be able to fit (more events than lines)
upcoming_events[:max_event_lines]
# Map days of month to co-ordinates of grid -> 3: (row2_x,col3_y)
grid = {}
for i in calendar_flat:
if i != 0:
grid[i] = grid_coordinates[calendar_flat.index(i)]
# logger.debug(f"grid:{grid}")
# remove zeros from calendar since they are not required
calendar_flat = [num for num in calendar_flat if num != 0]
# Check if any events were found in the given timerange
if upcoming_events:
# Add the numbers on the correct positions
for number in calendar_flat:
if number != int(now.day):
write(im_black, grid[number], (icon_width, icon_height),
str(number), font=self.num_font, fill_height=0.5, fill_width=0.5)
# Find out how much space (width) the date format requires
lang = self.language
# Draw a red/black circle with the current day of month in white
icon = Image.new('RGBA', (icon_width, icon_height))
current_day_pos = grid[int(now.day)]
x_circle, y_circle = int(icon_width / 2), int(icon_height / 2)
radius = int(icon_width * 0.2)
ImageDraw.Draw(icon).ellipse(
(x_circle - radius, y_circle - radius, x_circle + radius, y_circle + radius),
fill='black', outline=None)
write(icon, (0, 0), (icon_width, icon_height), str(now.day),
font=self.num_font, fill_height=0.5, colour='white')
im_colour.paste(icon, current_day_pos, icon)
date_width = int(max([self.font.getsize(
events['begin'].format(self.date_format,locale=lang))[0]
for events in upcoming_events]) * 1.1)
# If events should be loaded and shown...
if self.show_events == True:
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=lang))[0]
for events in upcoming_events]) * 1.1)
# If this month requires 5 instead of 6 rows, increase event section height
if len(cal.monthcalendar(now.year, now.month)) == 5:
events_height += icon_height
line_height = self.font.getsize('hg')[1] + line_spacing
# If this month requires 4 instead of 6 rows, increase event section height
elif len(cal.monthcalendar(now.year, now.month)) == 4:
events_height += icon_height * 2
event_width_s = im_width - date_width - time_width
event_width_l = im_width - date_width
# import the ical-parser
from inkycal.modules.ical_parser import iCalendar
# Display upcoming events below calendar
tomorrow = now.shift(days=1).floor('day')
in_two_days = now.shift(days=2).floor('day')
# find out how many lines can fit at max in the event section
line_spacing = 0
max_event_lines = events_height // (self.font.getsize('hg')[1] +
line_spacing)
cursor = 0
for event in upcoming_events:
if cursor < len(event_lines):
name = event['title']
date = event['begin'].format(self.date_format, locale=lang)
time = event['begin'].format(self.time_format, locale=lang)
#logger.debug(f"name:{name} date:{date} time:{time}")
# generate list of coordinates for each line
events_offset = im_height - events_height
event_lines = [(0, events_offset + int(events_height / max_event_lines * _))
for _ in range(max_event_lines)]
if now < event['end']:
write(im_colour, event_lines[cursor], (date_width, line_height),
date, font=self.font, alignment = 'left')
# logger.debug(f"event_lines {event_lines}")
# Check if event is all day
if parser.all_day(event) == True:
write(im_black, (date_width, event_lines[cursor][1]),
(event_width_l, line_height), name, font=self.font,
alignment = 'left')
else:
write(im_black, (date_width, event_lines[cursor][1]),
(time_width, line_height), time, font=self.font,
alignment = 'left')
# timeline for filtering events within this month
month_start = arrow.get(now.floor('month'))
month_end = arrow.get(now.ceil('month'))
write(im_black, (date_width+time_width,event_lines[cursor][1]),
(event_width_s, line_height), name, font=self.font,
alignment = 'left')
cursor += 1
else:
symbol = '- '
while self.font.getsize(symbol)[0] < im_width*0.9:
symbol += ' -'
write(im_black, event_lines[0],
(im_width, self.font.getsize(symbol)[1]), symbol,
font = self.font)
# fetch events from given icalendars
self.ical = iCalendar()
parser = self.ical
if self.ical_urls:
parser.load_url(self.ical_urls)
if self.ical_files:
parser.load_from_file(self.ical_files)
# Filter events for full month (even past ones) for drawing event icons
month_events = parser.get_events(month_start, month_end, self.timezone)
parser.sort()
self.month_events = month_events
# find out on which days of this month events are taking place
days_with_events = [int(events['begin'].format('D')) for events in
month_events]
# remove duplicates (more than one event in a single day)
list(set(days_with_events)).sort()
self._days_with_events = days_with_events
# Draw a border with specified parameters around days with events
for days in days_with_events:
if days in grid:
draw_border(
im_colour,
grid[days],
(icon_width, icon_height),
radius=6,
thickness=1,
shrinkage=(0.4, 0.2)
)
# Filter upcoming events until 4 weeks in the future
parser.clear_events()
upcoming_events = parser.get_events(now, now.shift(weeks=4),
self.timezone)
self._upcoming_events = upcoming_events
# delete events which won't be able to fit (more events than lines)
upcoming_events[:max_event_lines]
# Check if any events were found in the given timerange
if upcoming_events:
# Find out how much space (width) the date format requires
lang = self.language
date_width = int(max([self.font.getsize(
events['begin'].format(self.date_format, locale=lang))[0]
for events in upcoming_events]) * 1.1)
time_width = int(max([self.font.getsize(
events['begin'].format(self.time_format, locale=lang))[0]
for events in upcoming_events]) * 1.1)
line_height = self.font.getsize('hg')[1] + line_spacing
event_width_s = im_width - date_width - time_width
event_width_l = im_width - date_width
# Display upcoming events below calendar
tomorrow = now.shift(days=1).floor('day')
in_two_days = now.shift(days=2).floor('day')
cursor = 0
for event in upcoming_events:
if cursor < len(event_lines):
name = event['title']
date = event['begin'].format(self.date_format, locale=lang)
time = event['begin'].format(self.time_format, locale=lang)
# logger.debug(f"name:{name} date:{date} time:{time}")
if now < event['end']:
write(im_colour, event_lines[cursor], (date_width, line_height),
date, font=self.font, alignment='left')
# Check if event is all day
if parser.all_day(event) == True:
write(im_black, (date_width, event_lines[cursor][1]),
(event_width_l, line_height), name, font=self.font,
alignment='left')
else:
write(im_black, (date_width, event_lines[cursor][1]),
(time_width, line_height), time, font=self.font,
alignment='left')
write(im_black, (date_width + time_width, event_lines[cursor][1]),
(event_width_s, line_height), name, font=self.font,
alignment='left')
cursor += 1
else:
symbol = '- '
while self.font.getsize(symbol)[0] < im_width * 0.9:
symbol += ' -'
write(im_black, event_lines[0],
(im_width, self.font.getsize(symbol)[1]), symbol,
font=self.font)
# return the images ready for the display
return im_black, im_colour
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone mode')
print(f'running {filename} in standalone mode')

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Feeds module for InkyCal Project
@@ -11,147 +10,149 @@ from inkycal.modules.template import inkycal_module
from inkycal.custom import *
from random import shuffle
try:
import feedparser
import feedparser
except ImportError:
print('feedparser is not installed! Please install with:')
print('pip3 install feedparser')
print('feedparser is not installed! Please install with:')
print('pip3 install feedparser')
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Feeds(inkycal_module):
"""RSS class
parses rss/atom feeds from given urls
"""
"""RSS class
parses rss/atom feeds from given urls
"""
name = "RSS / Atom - Display feeds from given RSS/ATOM feeds"
name = "RSS / Atom - Display feeds from given RSS/ATOM feeds"
requires = {
"feed_urls" : {
"label":"Please enter ATOM or RSS feed URL/s, separated by a comma",
},
requires = {
"feed_urls": {
"label": "Please enter ATOM or RSS feed URL/s, separated by a comma",
},
}
optional = {
optional = {
"shuffle_feeds": {
"label": "Should the parsed RSS feeds be shuffled? (default=True)",
"options": [True, False],
"default": True
},
"shuffle_feeds": {
"label": "Should the parsed RSS feeds be shuffled? (default=True)",
"options": [True, False],
"default": True
},
}
def __init__(self, config):
"""Initialize inkycal_feeds module"""
def __init__(self, config):
"""Initialize inkycal_feeds module"""
super().__init__(config)
super().__init__(config)
config = config['config']
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# required parameters
if config["feed_urls"] and isinstance(config['feed_urls'], str):
self.feed_urls = config["feed_urls"].split(",")
else:
self.feed_urls = config["feed_urls"]
# required parameters
if config["feed_urls"] and isinstance(config['feed_urls'], str):
self.feed_urls = config["feed_urls"].split(",")
else:
self.feed_urls = config["feed_urls"]
# optional parameters
self.shuffle_feeds = config["shuffle_feeds"]
# optional parameters
self.shuffle_feeds = config["shuffle_feeds"]
# give an OK message
print(f'{filename} loaded')
# give an OK message
print(f'{filename} loaded')
def _validate(self):
"""Validate module-specific parameters"""
def _validate(self):
"""Validate module-specific parameters"""
if not isinstance(self.shuffle_feeds, bool):
print('shuffle_feeds has to be a boolean: True/False')
if not isinstance(self.shuffle_feeds, bool):
print('shuffle_feeds has to be a boolean: True/False')
def generate_image(self):
"""Generate image for this module"""
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
# 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')
# 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 :/')
# 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 feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
# 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)]
# Calculate line_positions
line_positions = [
(0, spacing_top + _ * line_height ) for _ in range(max_lines)]
# Create list containing all feeds from all urls
parsed_feeds = []
for feeds in self.feed_urls:
text = feedparser.parse(feeds)
for posts in text.entries:
summary = posts.summary
parsed_feeds.append(
f"{posts.title}: {re.sub('<[^<]+?>', '', posts.summary)}")
# Create list containing all feeds from all urls
parsed_feeds = []
for feeds in self.feed_urls:
text = feedparser.parse(feeds)
for posts in text.entries:
summary = posts.summary
parsed_feeds.append(
f"{posts.title}: {re.sub('<[^<]+?>', '', posts.summary)}")
self._parsed_feeds = parsed_feeds
self._parsed_feeds = parsed_feeds
# Shuffle the list to prevent showing the same content
if self.shuffle_feeds == True:
shuffle(parsed_feeds)
# Shuffle the list to prevent showing the same content
if self.shuffle_feeds == True:
shuffle(parsed_feeds)
# Trim down the list to the max number of lines
del parsed_feeds[max_lines:]
# Trim down the list to the max number of lines
del parsed_feeds[max_lines:]
# Wrap long text from feeds (line-breaking)
flatten = lambda z: [x for y in z for x in y]
filtered_feeds, counter = [], 0
# Wrap long text from feeds (line-breaking)
flatten = lambda z: [x for y in z for x in y]
filtered_feeds, counter = [], 0
for posts in parsed_feeds:
wrapped = text_wrap(posts, font=self.font, max_width=line_width)
counter += len(wrapped)
if counter < max_lines:
filtered_feeds.append(wrapped)
filtered_feeds = flatten(filtered_feeds)
self._filtered_feeds = filtered_feeds
for posts in parsed_feeds:
wrapped = text_wrap(posts, font = self.font, max_width = line_width)
counter += len(wrapped)
if counter < max_lines:
filtered_feeds.append(wrapped)
filtered_feeds = flatten(filtered_feeds)
self._filtered_feeds = filtered_feeds
logger.debug(f'filtered feeds -> {filtered_feeds}')
logger.debug(f'filtered feeds -> {filtered_feeds}')
# Check if feeds could be parsed and can be displayed
if len(filtered_feeds) == 0 and len(parsed_feeds) > 0:
print('Feeds could be parsed, but the text is too long to be displayed:/')
elif len(filtered_feeds) == 0 and len(parsed_feeds) == 0:
print('No feeds could be parsed :/')
else:
# Write 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')
# Check if feeds could be parsed and can be displayed
if len(filtered_feeds) == 0 and len(parsed_feeds) > 0:
print('Feeds could be parsed, but the text is too long to be displayed:/')
elif len(filtered_feeds) == 0 and len(parsed_feeds) == 0:
print('No feeds could be parsed :/')
else:
# Write 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')
# return images
return im_black, im_colour
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Image module for Inkycal Project
@@ -14,95 +13,96 @@ from inkycal.modules.inky_image import Inkyimage as Images
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Inkyimage(inkycal_module):
"""Displays an image from URL or local path
"""
"""Displays an image from URL or local path
"""
name = "Inkycal Image - show an image from a URL or local path"
name = "Inkycal Image - show an image from a URL or local path"
requires = {
"path":{
"label":"Path to a local folder, e.g. /home/pi/Desktop/images. "
"Only PNG and JPG/JPEG images are used for the slideshow."
},
requires = {
"palette": {
"label":"Which palette should be used for converting images?",
"options": ["bw", "bwr", "bwy"]
}
}
optional = {
"autoflip":{
"label":"Should the image be flipped automatically?",
"options": [True, False]
"path": {
"label": "Path to a local folder, e.g. /home/pi/Desktop/images. "
"Only PNG and JPG/JPEG images are used for the slideshow."
},
"orientation":{
"label": "Please select the desired orientation",
"options": ["vertical", "horizontal"]
}
"palette": {
"label": "Which palette should be used for converting images?",
"options": ["bw", "bwr", "bwy"]
}
}
def __init__(self, config):
"""Initialize module"""
optional = {
super().__init__(config)
"autoflip": {
"label": "Should the image be flipped automatically?",
"options": [True, False]
},
config = config['config']
"orientation": {
"label": "Please select the desired orientation",
"options": ["vertical", "horizontal"]
}
}
# required parameters
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
def __init__(self, config):
"""Initialize module"""
# optional parameters
self.path = config['path']
self.palette = config['palette']
self.autoflip = config['autoflip']
self.orientation = config['orientation']
super().__init__(config)
# give an OK message
print(f'{filename} loaded')
config = config['config']
# required parameters
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
def generate_image(self):
"""Generate image for this module"""
# optional parameters
self.path = config['path']
self.palette = config['palette']
self.autoflip = config['autoflip']
self.orientation = config['orientation']
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# give an OK message
print(f'{filename} loaded')
logger.info(f'Image size: {im_size}')
def generate_image(self):
"""Generate image for this module"""
# initialize custom image class
im = Images()
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# use the image at the first index
im.load(self.path)
logger.info(f'Image size: {im_size}')
# Remove background if present
im.remove_alpha()
# initialize custom image class
im = Images()
# if autoflip was enabled, flip the image
if self.autoflip == True:
im.autoflip(self.orientation)
# use the image at the first index
im.load(self.path)
# resize the image so it can fit on the epaper
im.resize( width=im_width, height=im_height )
# Remove background if present
im.remove_alpha()
# convert images according to specified palette
im_black, im_colour = im.to_palette(self.palette)
# if autoflip was enabled, flip the image
if self.autoflip == True:
im.autoflip(self.orientation)
# with the images now send, clear the current image
im.clear()
# resize the image so it can fit on the epaper
im.resize(width=im_width, height=im_height)
# convert images according to specified palette
im_black, im_colour = im.to_palette(self.palette)
# with the images now send, clear the current image
im.clear()
# return images
return im_black, im_colour
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
iCanHazDadJoke module for InkyCal Project
@@ -11,94 +10,95 @@ from inkycal.modules.template import inkycal_module
from inkycal.custom import *
import requests
# Show less logging for request module
logging.getLogger("urllib3").setLevel(logging.WARNING)
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Jokes(inkycal_module):
"""Icanhazdad-api class
parses rss/atom feeds from given urls
"""
"""Icanhazdad-api class
parses rss/atom feeds from given urls
"""
name = "iCanHazDad API - grab a random joke from icanhazdad api"
name = "iCanHazDad API - grab a random joke from icanhazdad api"
def __init__(self, config):
"""Initialize inkycal_feeds module"""
def __init__(self, config):
"""Initialize inkycal_feeds module"""
super().__init__(config)
super().__init__(config)
config = config['config']
config = config['config']
# give an OK message
print(f'{filename} loaded')
# give an OK message
print(f'{filename} loaded')
def generate_image(self):
"""Generate image for this module"""
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'image size: {im_width} x {im_height} px')
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'image size: {im_width} x {im_height} px')
# 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')
# 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 :/')
# 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 feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
logger.debug(f"max_lines: {max_lines}")
logger.debug(f"max_lines: {max_lines}")
# Calculate padding from top so the lines look centralised
spacing_top = int(im_height % line_height / 2)
# Calculate 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)]
# Calculate line_positions
line_positions = [
(0, spacing_top + _ * line_height ) for _ in range(max_lines)]
logger.debug(f'line positions: {line_positions}')
logger.debug(f'line positions: {line_positions}')
# Get the actual joke
url = "https://icanhazdadjoke.com"
header = {"accept": "text/plain"}
response = requests.get(url, headers=header)
response.encoding = 'utf-8' # Change encoding to UTF-8
joke = response.text.rstrip() # use to remove newlines
logger.debug(f"joke: {joke}")
# Get the actual joke
url = "https://icanhazdadjoke.com"
header = {"accept": "text/plain"}
response = requests.get(url, headers=header)
response.encoding = 'utf-8' # Change encoding to UTF-8
joke = response.text.rstrip() # use to remove newlines
logger.debug(f"joke: {joke}")
# wrap text in case joke is too large
wrapped = text_wrap(joke, font=self.font, max_width=line_width)
logger.debug(f"wrapped: {wrapped}")
# wrap text in case joke is too large
wrapped = text_wrap(joke, font = self.font, max_width = line_width)
logger.debug(f"wrapped: {wrapped}")
# Check if joke can actually fit on the provided space
if len(wrapped) > max_lines:
logger.error("Ohoh, Joke is too large for given space, please consider "
"increasing the size for this module")
# Check if joke can actually fit on the provided space
if len(wrapped) > max_lines:
logger.error("Ohoh, Joke is too large for given space, please consider "
"increasing the size for this module")
# Write the joke on the image
for _ in range(len(wrapped)):
if _ + 1 > max_lines:
logger.error('Ran out of lines for this joke :/')
break
write(im_black, line_positions[_], (line_width, line_height),
wrapped[_], font=self.font, alignment='left')
# Write the joke on the image
for _ in range(len(wrapped)):
if _+1 > max_lines:
logger.error('Ran out of lines for this joke :/')
break
write(im_black, line_positions[_], (line_width, line_height),
wrapped[_], font = self.font, alignment= 'left')
# Return images for black and colour channels
return im_black, im_colour
# Return images for black and colour channels
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Inkycal-server module for Inkycal Project
@@ -17,112 +16,113 @@ from inkycal.modules.inky_image import Inkyimage as Images
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Inkyserver(inkycal_module):
"""Displays an image from URL or local path
"""
"""Displays an image from URL or local path
"""
name = "Inykcal Server - fetches an image from Inkycal-server - (https://inkycal.robertsirre.nl/)"
name = "Inykcal Server - fetches an image from Inkycal-server - (https://inkycal.robertsirre.nl/)"
requires = {
requires = {
"path":{
"label": "Which URL should be used to get the image?"
},
"palette": {
"label":"Which palette should be used to convert the images?",
"options": ['bw', 'bwr', 'bwy']
}
}
optional = {
"path_body":{
"label":"Send this data to the server via POST. Use a comma to "
"separate multiple items",
"path": {
"label": "Which URL should be used to get the image?"
},
"dither":{
"label": "Dither images before sending to E-Paper? Default is False.",
"options": [False, True],
}
"palette": {
"label": "Which palette should be used to convert the images?",
"options": ['bw', 'bwr', 'bwy']
}
}
def __init__(self, config):
"""Initialize module"""
optional = {
super().__init__(config)
"path_body": {
"label": "Send this data to the server via POST. Use a comma to "
"separate multiple items",
},
"dither": {
"label": "Dither images before sending to E-Paper? Default is False.",
"options": [False, True],
}
config = config['config']
}
# required parameters
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
def __init__(self, config):
"""Initialize module"""
# optional parameters
self.path = config['path']
self.palette = config['palette']
self.dither = config['dither']
super().__init__(config)
# convert path_body to list, if not already
if config['path_body'] and isinstance(config['path_body'], str):
self.path_body = config['path_body'].split(',')
else:
self.path_body = config['path_body']
config = config['config']
# give an OK message
print(f'{filename} loaded')
# required parameters
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# optional parameters
self.path = config['path']
self.palette = config['palette']
self.dither = config['dither']
def generate_image(self):
"""Generate image for this module"""
# convert path_body to list, if not already
if config['path_body'] and isinstance(config['path_body'], str):
self.path_body = config['path_body'].split(',')
else:
self.path_body = config['path_body']
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# give an OK message
print(f'{filename} loaded')
logger.info(f'Image size: {im_size}')
def generate_image(self):
"""Generate image for this module"""
# replace width and height of url
print(self.path)
self.path = self.path.format(width=im_width, height=im_height)
print(f"modified path: {self.path}")
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# initialize custom image class
im = Images()
logger.info(f'Image size: {im_size}')
# when no path_body is provided, use plain GET
if not self.path_body:
# replace width and height of url
print(self.path)
self.path = self.path.format(width=im_width, height=im_height)
print(f"modified path: {self.path}")
# use the image at the first index
im.load(self.path)
# initialize custom image class
im = Images()
# else use POST request
else:
# Get the response image
response = Image.open(requests.post(
self.path, json=self.path_body, stream=True).raw)
# when no path_body is provided, use plain GET
if not self.path_body:
# initialize custom image class with response
im = Images(response)
# use the image at the first index
im.load(self.path)
# resize the image to respect padding
im.resize( width=im_width, height=im_height )
# else use POST request
else:
# Get the response image
response = Image.open(requests.post(
self.path, json=self.path_body, stream=True).raw)
# convert image to given palette
im_black, im_colour = im.to_palette(self.palette, dither=self.dither)
# initialize custom image class with response
im = Images(response)
# with the images now send, clear the current image
im.clear()
# resize the image to respect padding
im.resize(width=im_width, height=im_height)
# convert image to given palette
im_black, im_colour = im.to_palette(self.palette, dither=self.dither)
# with the images now send, clear the current image
im.clear()
# return images
return im_black, im_colour
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')
## 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}'
##path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height))
@@ -131,4 +131,3 @@ if __name__ == '__main__':
##inkycal_image_path_body = [
## 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics',
## 'https

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Image module for Inkycal Project
@@ -16,119 +15,121 @@ from inkycal.modules.inky_image import Inkyimage as Images
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Slideshow(inkycal_module):
"""Cycles through images in a local image folder
"""
name = "Slideshow - cycle through images from a local folder"
"""Cycles through images in a local image folder
"""
name = "Slideshow - cycle through images from a local folder"
requires = {
"path":{
"label":"Path to a local folder, e.g. /home/pi/Desktop/images. "
"Only PNG and JPG/JPEG images are used for the slideshow."
},
requires = {
"palette": {
"label":"Which palette should be used for converting images?",
"options": ["bw", "bwr", "bwy"]
}
}
optional = {
"autoflip":{
"label":"Should the image be flipped automatically? Default is False",
"options": [False, True]
"path": {
"label": "Path to a local folder, e.g. /home/pi/Desktop/images. "
"Only PNG and JPG/JPEG images are used for the slideshow."
},
"orientation":{
"label": "Please select the desired orientation",
"options": ["vertical", "horizontal"]
}
"palette": {
"label": "Which palette should be used for converting images?",
"options": ["bw", "bwr", "bwy"]
}
}
def __init__(self, config):
"""Initialize module"""
optional = {
super().__init__(config)
"autoflip": {
"label": "Should the image be flipped automatically? Default is False",
"options": [False, True]
},
config = config['config']
"orientation": {
"label": "Please select the desired orientation",
"options": ["vertical", "horizontal"]
}
}
# required parameters
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
def __init__(self, config):
"""Initialize module"""
# optional parameters
self.path = config['path']
self.palette = config['palette']
self.autoflip = config['autoflip']
self.orientation = config['orientation']
super().__init__(config)
# Get the full path of all png/jpg/jpeg images in the given folder
all_files = glob.glob(f'{self.path}/*')
self.images = [i for i in all_files
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
config = config['config']
if not self.images:
logger.error('No images found in the given folder, please '
'double check your path!')
raise Exception('No images found in the given folder path :/')
# required parameters
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# set a 'first run' signal
self._first_run = True
# optional parameters
self.path = config['path']
self.palette = config['palette']
self.autoflip = config['autoflip']
self.orientation = config['orientation']
# give an OK message
print(f'{filename} loaded')
# Get the full path of all png/jpg/jpeg images in the given folder
all_files = glob.glob(f'{self.path}/*')
self.images = [i for i in all_files
if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')]
def generate_image(self):
"""Generate image for this module"""
if not self.images:
logger.error('No images found in the given folder, please '
'double check your path!')
raise Exception('No images found in the given folder path :/')
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# set a 'first run' signal
self._first_run = True
logger.info(f'Image size: {im_size}')
# give an OK message
print(f'{filename} loaded')
# rotates list items by 1 index
def rotate(somelist):
return somelist[1:] + somelist[:1]
def generate_image(self):
"""Generate image for this module"""
# Switch to the next image if this is not the first run
if self._first_run == True:
self._first_run = False
else:
self.images = rotate(self.images)
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
# initialize custom image class
im = Images()
logger.info(f'Image size: {im_size}')
# temporary print method, prints current filename
print(f'slideshow - current image name: {self.images[0].split("/")[-1]}')
# rotates list items by 1 index
def rotate(somelist):
return somelist[1:] + somelist[:1]
# use the image at the first index
im.load(self.images[0])
# Switch to the next image if this is not the first run
if self._first_run == True:
self._first_run = False
else:
self.images = rotate(self.images)
# Remove background if present
im.remove_alpha()
# initialize custom image class
im = Images()
# if autoflip was enabled, flip the image
if self.autoflip == True:
im.autoflip(self.orientation)
# temporary print method, prints current filename
print(f'slideshow - current image name: {self.images[0].split("/")[-1]}')
# resize the image so it can fit on the epaper
im.resize( width=im_width, height=im_height )
# use the image at the first index
im.load(self.images[0])
# convert images according to specified palette
im_black, im_colour = im.to_palette(self.palette)
# Remove background if present
im.remove_alpha()
# with the images now send, clear the current image
im.clear()
# if autoflip was enabled, flip the image
if self.autoflip == True:
im.autoflip(self.orientation)
# resize the image so it can fit on the epaper
im.resize(width=im_width, height=im_height)
# convert images according to specified palette
im_black, im_colour = im.to_palette(self.palette)
# with the images now send, clear the current image
im.clear()
# return images
return im_black, im_colour
# return images
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Stocks Module for Inkycal Project
@@ -20,249 +20,252 @@ from inkycal.custom import write, internet_available
from PIL import Image
try:
import yfinance as yf
import yfinance as yf
except ImportError:
print('yfinance is not installed! Please install with:')
print('pip3 install yfinance')
print('yfinance is not installed! Please install with:')
print('pip3 install yfinance')
try:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
except ImportError:
print('matplotlib is not installed! Please install with:')
print('pip3 install matplotlib')
print('matplotlib is not installed! Please install with:')
print('pip3 install matplotlib')
logger = logging.getLogger(__name__)
class Stocks(inkycal_module):
name = "Stocks - Displays stock market infos from Yahoo finance"
name = "Stocks - Displays stock market infos from Yahoo finance"
# required parameters
requires = {
# required parameters
requires = {
"tickers": {
"tickers": {
"label": "You can display any information by using "
"the respective symbols that are used by Yahoo! Finance. "
"Separate multiple symbols with a comma sign e.g. "
"TSLA, U, NVDA, EURUSD=X"
}
"label": "You can display any information by using "
"the respective symbols that are used by Yahoo! Finance. "
"Separate multiple symbols with a comma sign e.g. "
"TSLA, U, NVDA, EURUSD=X"
}
}
def __init__(self, config):
def __init__(self, config):
super().__init__(config)
super().__init__(config)
config = config['config']
config = config['config']
# If tickers is a string from web-ui, convert to a list, else use
# tickers as-is i.e. for tests
if config['tickers'] and isinstance(config['tickers'], str):
self.tickers = config['tickers'].replace(" ", "").split(',') #returns list
else:
self.tickers = config['tickers']
# If tickers is a string from web-ui, convert to a list, else use
# tickers as-is i.e. for tests
if config['tickers'] and isinstance(config['tickers'], str):
self.tickers = config['tickers'].replace(" ", "").split(',') # returns list
else:
self.tickers = config['tickers']
# give an OK message
print(f'{__name__} loaded')
# give an OK message
print(f'{__name__} loaded')
def generate_image(self):
"""Generate image for this module"""
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'image size: {im_width} x {im_height} px')
# Define new image size with respect to padding
im_width = int(self.width - (2 * self.padding_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'image size: {im_width} x {im_height} px')
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size = im_size, color = 'white')
im_colour = Image.new('RGB', size = im_size, color = 'white')
# Create an image for black pixels and one for coloured pixels (required)
im_black = Image.new('RGB', size=im_size, color='white')
im_colour = Image.new('RGB', size=im_size, color='white')
# Create tmp path
tmpPath = '/tmp/inkycal_stocks/'
# Create tmp path
tmpPath = '/tmp/inkycal_stocks/'
try:
os.mkdir(tmpPath)
except OSError:
print (f"Creation of tmp directory {tmpPath} failed")
else:
print (f"Successfully created tmp directory {tmpPath} ")
try:
os.mkdir(tmpPath)
except OSError:
print(f"Creation of tmp directory {tmpPath} failed")
else:
print(f"Successfully created tmp directory {tmpPath} ")
# Check if internet is available
if internet_available() == True:
logger.info('Connection test passed')
else:
raise Exception('Network could not be reached :/')
# 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 feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
# Set some parameters for formatting feeds
line_spacing = 1
line_height = self.font.getsize('hg')[1] + line_spacing
line_width = im_width
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
logger.debug(f"max_lines: {max_lines}")
logger.debug(f"max_lines: {max_lines}")
# Calculate padding from top so the lines look centralised
spacing_top = int( im_height % line_height / 2 )
# Calculate 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)]
# Calculate line_positions
line_positions = [
(0, spacing_top + _ * line_height) for _ in range(max_lines)]
logger.debug(f'line positions: {line_positions}')
logger.debug(f'line positions: {line_positions}')
parsed_tickers = []
parsed_tickers_colour = []
chartSpace = Image.new('RGBA', (im_width, im_height), "white")
chartSpace_colour = Image.new('RGBA', (im_width, im_height), "white")
parsed_tickers = []
parsed_tickers_colour = []
chartSpace = Image.new('RGBA', (im_width, im_height), "white")
chartSpace_colour = Image.new('RGBA', (im_width, im_height), "white")
tickerCount = range(len(self.tickers))
tickerCount = range(len(self.tickers))
for _ in tickerCount:
ticker = self.tickers[_]
logger.info(f'preparing data for {ticker}...')
for _ in tickerCount:
ticker = self.tickers[_]
logger.info(f'preparing data for {ticker}...')
yfTicker = yf.Ticker(ticker)
yfTicker = yf.Ticker(ticker)
try:
stockInfo = yfTicker.info
except Exception as exceptionMessage:
logger.warning(f"Failed to get '{ticker}' ticker info: {exceptionMessage}")
try:
stockInfo = yfTicker.info
except Exception as exceptionMessage:
logger.warning(f"Failed to get '{ticker}' ticker info: {exceptionMessage}")
try:
stockName = stockInfo['shortName']
except Exception:
stockName = ticker
logger.warning(f"Failed to get '{stockName}' ticker name! Using "
"the ticker symbol as name instead.")
try:
stockName = stockInfo['shortName']
except Exception:
stockName = ticker
logger.warning(f"Failed to get '{stockName}' ticker name! Using "
"the ticker symbol as name instead.")
try:
stockCurrency = stockInfo['currency']
if stockCurrency == 'USD':
stockCurrency = '$'
elif stockCurrency == 'EUR':
stockCurrency = ''
except Exception:
stockCurrency = ''
logger.warning(f"Failed to get ticker currency!")
try:
precision = stockInfo['priceHint']
except Exception:
precision = 2
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
"default precision of 2 instead.")
try:
stockCurrency = stockInfo['currency']
if stockCurrency == 'USD':
stockCurrency = '$'
elif stockCurrency == 'EUR':
stockCurrency = ''
except Exception:
stockCurrency = ''
logger.warning(f"Failed to get ticker currency!")
stockHistory = yfTicker.history("30d")
stockHistoryLen = len(stockHistory)
logger.info(f'fetched {stockHistoryLen} datapoints ...')
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
currentQuote = (stockHistory.tail(1)['Close'].iloc[0])
currentHigh = (stockHistory.tail(1)['High'].iloc[0])
currentLow = (stockHistory.tail(1)['Low'].iloc[0])
currentOpen = (stockHistory.tail(1)['Open'].iloc[0])
currentGain = currentQuote-previousQuote
currentGainPercentage = (1-currentQuote/previousQuote)*-100
firstQuote = stockHistory.tail(stockHistoryLen)['Close'].iloc[0]
logger.info(f'firstQuote {firstQuote} ...')
def floatStr(precision, number):
return "%0.*f" % (precision, number)
def percentageStr(number):
return '({:+.2f}%)'.format(number)
def gainStr(precision, number):
return "%+.*f" % (precision, number)
try:
precision = stockInfo['priceHint']
except Exception:
precision = 2
logger.warning(f"Failed to get '{stockName}' ticker price hint! Using "
"default precision of 2 instead.")
stockNameLine = '{} ({})'.format(stockName, stockCurrency)
stockCurrentValueLine = '{} {} {}'.format(
floatStr(precision, currentQuote), gainStr(precision, currentGain), percentageStr(currentGainPercentage))
stockDayValueLine = '1d OHL: {}/{}/{}'.format(
floatStr(precision, currentOpen), floatStr(precision, currentHigh), floatStr(precision, currentLow))
maxQuote = max(stockHistory.High)
minQuote = min(stockHistory.Low)
logger.info(f'high {maxQuote} low {minQuote} ...')
stockMonthValueLine = '{}d OHL: {}/{}/{}'.format(
stockHistoryLen,floatStr(precision, firstQuote),floatStr(precision, maxQuote),floatStr(precision, minQuote))
stockHistory = yfTicker.history("30d")
stockHistoryLen = len(stockHistory)
logger.info(f'fetched {stockHistoryLen} datapoints ...')
previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
currentQuote = (stockHistory.tail(1)['Close'].iloc[0])
currentHigh = (stockHistory.tail(1)['High'].iloc[0])
currentLow = (stockHistory.tail(1)['Low'].iloc[0])
currentOpen = (stockHistory.tail(1)['Open'].iloc[0])
currentGain = currentQuote - previousQuote
currentGainPercentage = (1 - currentQuote / previousQuote) * -100
firstQuote = stockHistory.tail(stockHistoryLen)['Close'].iloc[0]
logger.info(f'firstQuote {firstQuote} ...')
logger.info(stockNameLine)
logger.info(stockCurrentValueLine)
logger.info(stockDayValueLine)
logger.info(stockMonthValueLine)
parsed_tickers.append(stockNameLine)
parsed_tickers.append(stockCurrentValueLine)
parsed_tickers.append(stockDayValueLine)
parsed_tickers.append(stockMonthValueLine)
def floatStr(precision, number):
return "%0.*f" % (precision, number)
parsed_tickers_colour.append("")
if currentGain < 0:
parsed_tickers_colour.append(stockCurrentValueLine)
else:
parsed_tickers_colour.append("")
if currentOpen > currentQuote:
parsed_tickers_colour.append(stockDayValueLine)
else:
parsed_tickers_colour.append("")
if firstQuote > currentQuote:
parsed_tickers_colour.append(stockMonthValueLine)
else:
parsed_tickers_colour.append("")
def percentageStr(number):
return '({:+.2f}%)'.format(number)
if (_ < len(tickerCount)):
parsed_tickers.append("")
parsed_tickers_colour.append("")
def gainStr(precision, number):
return "%+.*f" % (precision, number)
logger.info(f'creating chart data...')
chartData = stockHistory.reset_index()
chartCloseData = chartData.loc[:,'Close']
chartTimeData = chartData.loc[:,'Date']
stockNameLine = '{} ({})'.format(stockName, stockCurrency)
stockCurrentValueLine = '{} {} {}'.format(
floatStr(precision, currentQuote), gainStr(precision, currentGain),
percentageStr(currentGainPercentage))
stockDayValueLine = '1d OHL: {}/{}/{}'.format(
floatStr(precision, currentOpen), floatStr(precision, currentHigh), floatStr(precision, currentLow))
maxQuote = max(stockHistory.High)
minQuote = min(stockHistory.Low)
logger.info(f'high {maxQuote} low {minQuote} ...')
stockMonthValueLine = '{}d OHL: {}/{}/{}'.format(
stockHistoryLen, floatStr(precision, firstQuote), floatStr(precision, maxQuote),
floatStr(precision, minQuote))
logger.info(f'creating chart plot...')
fig, ax = plt.subplots() # Create a figure containing a single axes.
ax.plot(chartTimeData, chartCloseData, linewidth=8) # Plot some data on the axes.
ax.set_xticklabels([])
ax.set_yticklabels([])
chartPath = tmpPath+ticker+'.png'
logger.info(f'saving chart image to {chartPath}...')
plt.savefig(chartPath)
logger.info(stockNameLine)
logger.info(stockCurrentValueLine)
logger.info(stockDayValueLine)
logger.info(stockMonthValueLine)
parsed_tickers.append(stockNameLine)
parsed_tickers.append(stockCurrentValueLine)
parsed_tickers.append(stockDayValueLine)
parsed_tickers.append(stockMonthValueLine)
logger.info(f'chartSpace is...{im_width} {im_height}')
logger.info(f'open chart ...{chartPath}')
chartImage = Image.open(chartPath)
chartImage.thumbnail((im_width/4,line_height*4), Image.BICUBIC)
parsed_tickers_colour.append("")
if currentGain < 0:
parsed_tickers_colour.append(stockCurrentValueLine)
else:
parsed_tickers_colour.append("")
if currentOpen > currentQuote:
parsed_tickers_colour.append(stockDayValueLine)
else:
parsed_tickers_colour.append("")
if firstQuote > currentQuote:
parsed_tickers_colour.append(stockMonthValueLine)
else:
parsed_tickers_colour.append("")
chartPasteX = im_width-(chartImage.width)
chartPasteY = line_height*5*_
logger.info(f'pasting chart image with index {_} to...{chartPasteX} {chartPasteY}')
if (_ < len(tickerCount)):
parsed_tickers.append("")
parsed_tickers_colour.append("")
if firstQuote > currentQuote:
chartSpace_colour.paste(chartImage, (chartPasteX, chartPasteY))
else:
chartSpace.paste(chartImage, (chartPasteX, chartPasteY))
logger.info(f'creating chart data...')
chartData = stockHistory.reset_index()
chartCloseData = chartData.loc[:, 'Close']
chartTimeData = chartData.loc[:, 'Date']
im_black.paste(chartSpace)
im_colour.paste(chartSpace_colour)
logger.info(f'creating chart plot...')
fig, ax = plt.subplots() # Create a figure containing a single axes.
ax.plot(chartTimeData, chartCloseData, linewidth=8) # Plot some data on the axes.
ax.set_xticklabels([])
ax.set_yticklabels([])
chartPath = tmpPath + ticker + '.png'
logger.info(f'saving chart image to {chartPath}...')
plt.savefig(chartPath)
# Write/Draw something on the black image
for _ in range(len(parsed_tickers)):
if _+1 > max_lines:
logger.error('Ran out of lines for parsed_ticker_colour')
break
write(im_black, line_positions[_], (line_width, line_height),
parsed_tickers[_], font = self.font, alignment= 'left')
logger.info(f'chartSpace is...{im_width} {im_height}')
logger.info(f'open chart ...{chartPath}')
chartImage = Image.open(chartPath)
chartImage.thumbnail((im_width / 4, line_height * 4), Image.BICUBIC)
# Write/Draw something on the colour image
for _ in range(len(parsed_tickers_colour)):
if _+1 > max_lines:
logger.error('Ran out of lines for parsed_tickers_colour')
break
write(im_colour, line_positions[_], (line_width, line_height),
parsed_tickers_colour[_], font = self.font, alignment= 'left')
chartPasteX = im_width - (chartImage.width)
chartPasteY = line_height * 5 * _
logger.info(f'pasting chart image with index {_} to...{chartPasteX} {chartPasteY}')
if firstQuote > currentQuote:
chartSpace_colour.paste(chartImage, (chartPasteX, chartPasteY))
else:
chartSpace.paste(chartImage, (chartPasteX, chartPasteY))
im_black.paste(chartSpace)
im_colour.paste(chartSpace_colour)
# Write/Draw something on the black image
for _ in range(len(parsed_tickers)):
if _ + 1 > max_lines:
logger.error('Ran out of lines for parsed_ticker_colour')
break
write(im_black, line_positions[_], (line_width, line_height),
parsed_tickers[_], font=self.font, alignment='left')
# Write/Draw something on the colour image
for _ in range(len(parsed_tickers_colour)):
if _ + 1 > max_lines:
logger.error('Ran out of lines for parsed_tickers_colour')
break
write(im_colour, line_positions[_], (line_width, line_height),
parsed_tickers_colour[_], font=self.font, alignment='left')
# Save image of black and colour channel in image-folder
return im_black, im_colour
# Save image of black and colour channel in image-folder
return im_black, im_colour
if __name__ == '__main__':
print('running module in standalone/debug mode')
print('running module in standalone/debug mode')

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
todoist module for Inky-Calendar Project
@@ -10,195 +9,196 @@ from inkycal.modules.template import inkycal_module
from inkycal.custom import *
try:
import todoist
import todoist
except ImportError:
print('todoist is not installed! Please install with:')
print('pip3 install todoist-python')
print('todoist is not installed! Please install with:')
print('pip3 install todoist-python')
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Todoist(inkycal_module):
"""Todoist api class
parses todo's from api-key
"""
"""Todoist api class
parses todo's from api-key
"""
name = "Todoist API - show your todos from todoist"
name = "Todoist API - show your todos from 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",
requires = {
'api_key': {
"label": "Please enter your Todoist API-key",
},
}
}
def __init__(self, config):
"""Initialize inkycal_rss module"""
optional = {
'project_filter': {
"label": "Show Todos only from following project (separated by a comma). Leave empty to show " +
"todos from all projects",
}
}
super().__init__(config)
def __init__(self, config):
"""Initialize inkycal_rss module"""
config = config['config']
super().__init__(config)
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
config = config['config']
# module specific parameters
self.api_key = config['api_key']
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# if project filter is set, initialize it
if config['project_filter'] and isinstance(config['project_filter'], str):
self.project_filter = config['project_filter'].split(',')
else:
self.project_filter = config['project_filter']
# module specific parameters
self.api_key = config['api_key']
self._api = todoist.TodoistAPI(config['api_key'])
self._api.sync()
# give an OK message
print(f'{filename} loaded')
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
# 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')
self._api.sync()
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['id']: project['name']
for project in self._api.projects.all()}
logger.debug(f"all_projects: {all_projects}")
# Filter entries in all_projects if filter was given
if self.project_filter:
for project_id in list(all_projects):
if all_projects[project_id] not in self.project_filter:
del all_projects[project_id]
logger.debug(f"all_project: {all_projects}")
# If filter was activated and no roject was found with that name,
# raise an exception to avoid showing a blank image
if all_projects == {}:
logger.error('No project found from project filter!')
logger.error('Please double check spellings in project_filter')
raise Exception('No matching project found in filter. Please '
'double check spellings in project_filter or leave'
'empty')
# Create single-use generator to filter undone and non-deleted tasks
tasks = (task.data for task in self._api.state['items'] if
task['checked'] == 0 and task['is_deleted']==0)
# Simplify the tasks for faster processing
simplified = [
{
'name':task['content'],
'due':task['due']['string'] if task['due'] != None else "",
'priority':task['priority'],
'project':all_projects[ task['project_id' ] ] if task['project_id'] in all_projects else "deleted"
}
for task in tasks]
# remove groups that have been deleted
simplified = [task for task in simplified if task['project'] != "deleted"]
logger.debug(f'simplified: {simplified}')
# Get maximum width of project names for selected font
project_width = int(max([
self.font.getsize(task['project'])[0] for task in simplified ]) * 1.1)
# Get maximum width of project dues for selected font
due_width = int(max([
self.font.getsize(task['due'])[0] for task in simplified ]) * 1.1)
# Group tasks by project name
grouped = {name: [] for id_, name in all_projects.items()}
for task in simplified:
if task['project'] in grouped:
grouped[task['project']].append(task)
logger.debug(f"grouped: {grouped}")
# Add the parsed todos on the image
cursor = 0
for name, todos in grouped.items():
if todos:
for todo in todos:
if cursor < len(line_positions):
line_x, line_y = line_positions[cursor]
# Add todo project name
write(
im_colour, line_positions[cursor],
(project_width, line_height),
todo['project'], font=self.font, alignment='left')
# Add todo due if not empty
if todo['due'] != "":
write(
im_black,
(line_x + project_width, line_y),
(due_width, line_height),
todo['due'], font=self.font, alignment='left')
# Add todo name
write(
im_black,
(line_x+project_width+due_width, line_y),
(im_width-project_width-due_width, line_height),
todo['name'], font=self.font, alignment='left')
cursor += 1
# if project filter is set, initialize it
if config['project_filter'] and isinstance(config['project_filter'], str):
self.project_filter = config['project_filter'].split(',')
else:
logger.error('More todos than available lines')
break
self.project_filter = config['project_filter']
self._api = todoist.TodoistAPI(config['api_key'])
self._api.sync()
# give an OK message
print(f'{filename} loaded')
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
# 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')
self._api.sync()
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['id']: project['name']
for project in self._api.projects.all()}
logger.debug(f"all_projects: {all_projects}")
# Filter entries in all_projects if filter was given
if self.project_filter:
for project_id in list(all_projects):
if all_projects[project_id] not in self.project_filter:
del all_projects[project_id]
logger.debug(f"all_project: {all_projects}")
# If filter was activated and no roject was found with that name,
# raise an exception to avoid showing a blank image
if all_projects == {}:
logger.error('No project found from project filter!')
logger.error('Please double check spellings in project_filter')
raise Exception('No matching project found in filter. Please '
'double check spellings in project_filter or leave'
'empty')
# Create single-use generator to filter undone and non-deleted tasks
tasks = (task.data for task in self._api.state['items'] if
task['checked'] == 0 and task['is_deleted'] == 0)
# Simplify the tasks for faster processing
simplified = [
{
'name': task['content'],
'due': task['due']['string'] if task['due'] != None else "",
'priority': task['priority'],
'project': all_projects[task['project_id']] if task['project_id'] in all_projects else "deleted"
}
for task in tasks]
# remove groups that have been deleted
simplified = [task for task in simplified if task['project'] != "deleted"]
logger.debug(f'simplified: {simplified}')
# Get maximum width of project names for selected font
project_width = int(max([
self.font.getsize(task['project'])[0] for task in simplified]) * 1.1)
# Get maximum width of project dues for selected font
due_width = int(max([
self.font.getsize(task['due'])[0] for task in simplified]) * 1.1)
# Group tasks by project name
grouped = {name: [] for id_, name in all_projects.items()}
for task in simplified:
if task['project'] in grouped:
grouped[task['project']].append(task)
logger.debug(f"grouped: {grouped}")
# Add the parsed todos on the image
cursor = 0
for name, todos in grouped.items():
if todos:
for todo in todos:
if cursor < len(line_positions):
line_x, line_y = line_positions[cursor]
# Add todo project name
write(
im_colour, line_positions[cursor],
(project_width, line_height),
todo['project'], font=self.font, alignment='left')
# Add todo due if not empty
if todo['due'] != "":
write(
im_black,
(line_x + project_width, line_y),
(due_width, line_height),
todo['due'], font=self.font, alignment='left')
# Add todo name
write(
im_black,
(line_x + project_width + due_width, line_y),
(im_width - project_width - due_width, line_height),
todo['name'], font=self.font, alignment='left')
cursor += 1
else:
logger.error('More todos than available lines')
break
# return the images ready for the display
return im_black, im_colour
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone/debug mode')
print(f'running {filename} in standalone/debug mode')

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Weather module for Inky-Calendar software.
Copyright by aceisace
@@ -13,510 +13,502 @@ import arrow
from locale import getdefaultlocale as sys_locale
try:
from pyowm.owm import OWM
from pyowm.owm import OWM
except ImportError:
print('pyowm is not installed! Please install with:')
print('pip3 install pyowm')
print('pyowm is not installed! Please install with:')
print('pip3 install pyowm')
filename = os.path.basename(__file__).split('.py')[0]
logger = logging.getLogger(filename)
class Weather(inkycal_module):
"""Weather class
parses weather details from openweathermap
"""
name = "Weather (openweathermap) - Get weather forecasts from openweathermap"
"""Weather class
parses weather details from openweathermap
"""
name = "Weather (openweathermap) - Get weather forecasts from openweathermap"
requires = {
requires = {
"api_key" : {
"label":"Please enter openweathermap api-key. You can create one for free on openweathermap",
},
"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. "+
"You can also enter the location ID found in the url "+
"e.g. https://openweathermap.org/city/4893171 -> ID is 4893171"
}
"location": {
"label": "Please enter your location in the following format: City, Country-Code. " +
"You can also enter the location ID found in the url " +
"e.g. https://openweathermap.org/city/4893171 -> ID is 4893171"
}
}
optional = {
optional = {
"round_temperature": {
"label":"Round temperature to the nearest degree?",
"options": [True, False],
},
"round_temperature": {
"label": "Round temperature to the nearest degree?",
"options": [True, False],
},
"round_windspeed": {
"label":"Round windspeed?",
"options": [True, False],
},
"round_windspeed": {
"label": "Round windspeed?",
"options": [True, False],
},
"forecast_interval": {
"label":"Please select the forecast interval",
"options": ["daily", "hourly"],
},
"forecast_interval": {
"label": "Please select the forecast interval",
"options": ["daily", "hourly"],
},
"units": {
"label": "Which units should be used?",
"options": ["metric", "imperial"],
},
"units": {
"label": "Which units should be used?",
"options": ["metric", "imperial"],
},
"hour_format": {
"label": "Which hour format do you prefer?",
"options": [24, 12],
},
"hour_format": {
"label": "Which hour format do you prefer?",
"options": [24, 12],
},
"use_beaufort": {
"label": "Use beaufort scale for windspeed?",
"options": [True, False],
},
"use_beaufort": {
"label": "Use beaufort scale for windspeed?",
"options": [True, False],
},
}
def __init__(self, config):
"""Initialize inkycal_weather module"""
super().__init__(config)
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# required parameters
self.api_key = config['api_key']
self.location = config['location']
# optional parameters
self.round_temperature = config['round_temperature']
self.round_windspeed = config['round_windspeed']
self.forecast_interval = config['forecast_interval']
self.units = config['units']
self.hour_format = int(config['hour_format'])
self.use_beaufort = config['use_beaufort']
# additional configuration
self.owm = OWM(self.api_key).weather_manager()
self.timezone = get_system_tz()
self.locale = config['language']
self.weatherfont = ImageFont.truetype(
fonts['weathericons-regular-webfont'], size = self.fontsize)
# give an OK message
print(f"{filename} loaded")
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
# 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:
logger.exception('Network could not be reached :(')
raise
def get_moon_phase():
"""Calculate the current (approximate) moon phase"""
dec = decimal.Decimal
diff = now - arrow.get(2001, 1, 1)
days = dec(diff.days) + (dec(diff.seconds) / dec(86400))
lunations = dec("0.20439731") + (days * dec("0.03386319269"))
position = lunations % dec(1)
index = math.floor((position * dec(8)) + dec("0.5"))
return {0: '\uf095',1: '\uf099',2: '\uf09c',3: '\uf0a0',
4: '\uf0a3',5: '\uf0a7',6: '\uf0aa',7: '\uf0ae' }[int(index) & 7]
def is_negative(temp):
"""Check if temp is below freezing point of water (0°C/30°F)
returns True if temp below freezing point, else False"""
answer = False
if temp_unit == 'celsius' and round(float(temp.split('°')[0])) <= 0:
answer = True
elif temp_unit == 'fahrenheit' and round(float(temp.split('°')[0])) <= 0:
answer = True
return answer
# Lookup-table for weather icons and weather codes
weathericons = {
'01d': '\uf00d', '02d': '\uf002', '03d': '\uf013',
'04d': '\uf012', '09d': '\uf01a ', '10d': '\uf019',
'11d': '\uf01e', '13d': '\uf01b', '50d': '\uf014',
'01n': '\uf02e', '02n': '\uf013', '03n': '\uf013',
'04n': '\uf013', '09n': '\uf037', '10n': '\uf036',
'11n': '\uf03b', '13n': '\uf038', '50n': '\uf023'
}
def draw_icon(image, xy, box_size, icon, rotation = None):
"""Custom function to add icons of weather font on image
image = on which image should the text be added?
xy = xy-coordinates as tuple -> (x,y)
box_size = size of text-box -> (width,height)
icon = icon-unicode, looks this up in weathericons dictionary
"""
x,y = xy
box_width, box_height = box_size
text = icon
font = self.weatherfont
# Increase fontsize to fit specified height and width of text box
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
while (text_width < int(box_width * 0.9) and
text_height < int(box_height * 0.9)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getsize(text)
# Align text to desired position
x = int((box_width / 2) - (text_width / 2))
y = int((box_height / 2) - (text_height / 2))
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
if rotation != None:
space.rotate(rotation, expand = True)
# Update only region with text (add text with transparent background)
image.paste(space, xy, space)
# column1 column2 column3 column4 column5 column6 column7
# |----------|----------|----------|----------|----------|----------|----------|
# | time | temperat.| moonphase| forecast1| forecast2| forecast3| forecast4|
# | current |----------|----------|----------|----------|----------|----------|
# | weather | humidity | sunrise | icon1 | icon2 | icon3 | icon4 |
# | icon |----------|----------|----------|----------|----------|----------|
# | | windspeed| sunset | temperat.| temperat.| temperat.| temperat.|
# |----------|----------|----------|----------|----------|----------|----------|
# Calculate size rows and columns
col_width = im_width // 7
# Ratio width height
image_ratio = im_width / im_height
if image_ratio >= 4:
row_height = im_height // 3
else:
logger.info('Please consider decreasing the height.')
row_height = int( (im_height* (1-im_height/im_width)) / 3 )
logger.debug(f"row_height: {row_height} | col_width: {col_width}")
# Calculate spacings for better centering
spacing_top = int( (im_width % col_width) / 2 )
spacing_left = int( (im_height % row_height) / 2 )
# Define sizes for weather icons
icon_small = int(col_width / 3)
icon_medium = icon_small * 2
icon_large = icon_small * 3
# Calculate the x-axis position of each col
col1 = spacing_top
col2 = col1 + col_width
col3 = col2 + col_width
col4 = col3 + col_width
col5 = col4 + col_width
col6 = col5 + col_width
col7 = col6 + col_width
# Calculate the y-axis position of each row
line_gap = int((im_height - spacing_top - 3*row_height) // 4)
row1 = line_gap
row2 = row1 + line_gap + row_height
row3 = row2+ line_gap + row_height
# Draw lines on each row and border
############################################################################
## draw = ImageDraw.Draw(im_black)
## draw.line((0, 0, im_width, 0), fill='red')
## draw.line((0, im_height-1, im_width, im_height-1), fill='red')
## draw.line((0, row1, im_width, row1), fill='black')
## draw.line((0, row1+row_height, im_width, row1+row_height), fill='black')
## draw.line((0, row2, im_width, row2), fill='black')
## draw.line((0, row2+row_height, im_width, row2+row_height), fill='black')
## draw.line((0, row3, im_width, row3), fill='black')
## draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
############################################################################
# Positions for current weather details
weather_icon_pos = (col1, 0)
temperature_icon_pos = (col2, row1)
temperature_pos = (col2+icon_small, row1)
humidity_icon_pos = (col2, row2)
humidity_pos = (col2+icon_small, row2)
windspeed_icon_pos = (col2, row3)
windspeed_pos = (col2+icon_small, row3)
# Positions for sunrise, sunset, moonphase
moonphase_pos = (col3, row1)
sunrise_icon_pos = (col3, row2)
sunrise_time_pos = (col3+icon_small, row2)
sunset_icon_pos = (col3, row3)
sunset_time_pos = (col3+ icon_small, row3)
# Positions for forecast 1
stamp_fc1 = (col4, row1)
icon_fc1 = (col4, row1+row_height)
temp_fc1 = (col4, row3)
# Positions for forecast 2
stamp_fc2 = (col5, row1)
icon_fc2 = (col5, row1+row_height)
temp_fc2 = (col5, row3)
# Positions for forecast 3
stamp_fc3 = (col6, row1)
icon_fc3 = (col6, row1+row_height)
temp_fc3 = (col6, row3)
# Positions for forecast 4
stamp_fc4 = (col7, row1)
icon_fc4 = (col7, row1+row_height)
temp_fc4 = (col7, row3)
# Create current-weather and weather-forecast objects
if self.location.isdigit():
logging.debug('looking up location by ID')
weather = self.owm.weather_at_id(int(self.location)).weather
forecast = self.owm.forecast_at_id(int(self.location), '3h')
else:
logging.debug('looking up location by string')
weather = self.owm.weather_at_place(self.location).weather
forecast = self.owm.forecast_at_place(self.location, '3h')
# Set decimals
dec_temp = None if self.round_temperature == True else 1
dec_wind = None if self.round_windspeed == True else 1
# Set correct temperature units
if self.units == 'metric':
temp_unit = 'celsius'
elif self.units == 'imperial':
temp_unit = 'fahrenheit'
logging.debug(f'temperature unit: {temp_unit}')
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
# Get current time
now = arrow.utcnow()
if self.forecast_interval == 'hourly':
logger.debug("getting hourly forecasts")
# Forecasts are provided for every 3rd full hour
# find out how many hours there are until the next 3rd full hour
if (now.hour % 3) != 0:
hour_gap = 3 - (now.hour % 3)
else:
hour_gap = 3
# Create timings for hourly forcasts
forecast_timings = [now.shift(hours = + hour_gap + _).floor('hour')
for _ in range(0,12,3)]
# Create forecast objects for given timings
forecasts = [forecast.get_weather_at(forecast_time.datetime) for
forecast_time in forecast_timings]
# Add forecast-data to fc_data dictionary
fc_data = {}
for forecast in forecasts:
temp = '{}°'.format(round(
forecast.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
icon = forecast.weather_icon_name
fc_data['fc'+str(forecasts.index(forecast)+1)] = {
'temp':temp,
'icon':icon,
'stamp': forecast_timings[forecasts.index(forecast)].to(
get_system_tz()).format('H.00' if self.hour_format == 24 else 'h a')
}
elif self.forecast_interval == 'daily':
logger.debug("getting daily forecasts")
def calculate_forecast(days_from_today):
"""Get temperature range and most frequent icon code for forecast
days_from_today should be int from 1-4: e.g. 2 -> 2 days from today
"""
# Create a list containing time-objects for every 3rd hour of the day
time_range = list(arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),
now.shift(days=days_from_today).ceil('day')
))[::3]
# Get forecasts for each time-object
forecasts = [forecast.get_weather_at(_.datetime) for _ in time_range]
# Get all temperatures for this day
daily_temp = [round(_.temperature(unit=temp_unit)['temp'],
ndigits=dec_temp) for _ in forecasts]
# Calculate min. and max. temp for this day
temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°'
# Get all weather icon codes for this day
daily_icons = [_.weather_icon_name for _ in forecasts]
# Find most common element from all weather icon codes
status = max(set(daily_icons), key=daily_icons.count)
weekday = now.shift(days=days_from_today).format('ddd', locale=
self.locale)
return {'temp':temp_range, 'icon':status, 'stamp': weekday}
forecasts = [calculate_forecast(days) for days in range (1,5)]
fc_data = {}
for forecast in forecasts:
fc_data['fc'+str(forecasts.index(forecast)+1)] = {
'temp':forecast['temp'],
'icon':forecast['icon'],
'stamp': forecast['stamp']
}
def __init__(self, config):
"""Initialize inkycal_weather module"""
super().__init__(config)
config = config['config']
# Check if all required parameters are present
for param in self.requires:
if not param in config:
raise Exception(f'config is missing {param}')
# required parameters
self.api_key = config['api_key']
self.location = config['location']
# optional parameters
self.round_temperature = config['round_temperature']
self.round_windspeed = config['round_windspeed']
self.forecast_interval = config['forecast_interval']
self.units = config['units']
self.hour_format = int(config['hour_format'])
self.use_beaufort = config['use_beaufort']
# additional configuration
self.owm = OWM(self.api_key).weather_manager()
self.timezone = get_system_tz()
self.locale = config['language']
self.weatherfont = ImageFont.truetype(
fonts['weathericons-regular-webfont'], size=self.fontsize)
# give an OK message
print(f"{filename} loaded")
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_left))
im_height = int(self.height - (2 * self.padding_top))
im_size = im_width, im_height
logger.info(f'Image size: {im_size}')
# 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:
logger.exception('Network could not be reached :(')
raise
def get_moon_phase():
"""Calculate the current (approximate) moon phase"""
dec = decimal.Decimal
diff = now - arrow.get(2001, 1, 1)
days = dec(diff.days) + (dec(diff.seconds) / dec(86400))
lunations = dec("0.20439731") + (days * dec("0.03386319269"))
position = lunations % dec(1)
index = math.floor((position * dec(8)) + dec("0.5"))
return {0: '\uf095', 1: '\uf099', 2: '\uf09c', 3: '\uf0a0',
4: '\uf0a3', 5: '\uf0a7', 6: '\uf0aa', 7: '\uf0ae'}[int(index) & 7]
def is_negative(temp):
"""Check if temp is below freezing point of water (0°C/30°F)
returns True if temp below freezing point, else False"""
answer = False
if temp_unit == 'celsius' and round(float(temp.split('°')[0])) <= 0:
answer = True
elif temp_unit == 'fahrenheit' and round(float(temp.split('°')[0])) <= 0:
answer = True
return answer
# Lookup-table for weather icons and weather codes
weathericons = {
'01d': '\uf00d', '02d': '\uf002', '03d': '\uf013',
'04d': '\uf012', '09d': '\uf01a ', '10d': '\uf019',
'11d': '\uf01e', '13d': '\uf01b', '50d': '\uf014',
'01n': '\uf02e', '02n': '\uf013', '03n': '\uf013',
'04n': '\uf013', '09n': '\uf037', '10n': '\uf036',
'11n': '\uf03b', '13n': '\uf038', '50n': '\uf023'
}
def draw_icon(image, xy, box_size, icon, rotation=None):
"""Custom function to add icons of weather font on image
image = on which image should the text be added?
xy = xy-coordinates as tuple -> (x,y)
box_size = size of text-box -> (width,height)
icon = icon-unicode, looks this up in weathericons dictionary
"""
x, y = xy
box_width, box_height = box_size
text = icon
font = self.weatherfont
# Increase fontsize to fit specified height and width of text box
size = 8
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
while (text_width < int(box_width * 0.9) and
text_height < int(box_height * 0.9)):
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
text_width, text_height = font.getsize(text)
# Align text to desired position
x = int((box_width / 2) - (text_width / 2))
y = int((box_height / 2) - (text_height / 2))
# Draw the text in the text-box
draw = ImageDraw.Draw(image)
space = Image.new('RGBA', (box_width, box_height))
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
if rotation != None:
space.rotate(rotation, expand=True)
# Update only region with text (add text with transparent background)
image.paste(space, xy, space)
# column1 column2 column3 column4 column5 column6 column7
# |----------|----------|----------|----------|----------|----------|----------|
# | time | temperat.| moonphase| forecast1| forecast2| forecast3| forecast4|
# | current |----------|----------|----------|----------|----------|----------|
# | weather | humidity | sunrise | icon1 | icon2 | icon3 | icon4 |
# | icon |----------|----------|----------|----------|----------|----------|
# | | windspeed| sunset | temperat.| temperat.| temperat.| temperat.|
# |----------|----------|----------|----------|----------|----------|----------|
# Calculate size rows and columns
col_width = im_width // 7
# Ratio width height
image_ratio = im_width / im_height
if image_ratio >= 4:
row_height = im_height // 3
else:
logger.info('Please consider decreasing the height.')
row_height = int((im_height * (1 - im_height / im_width)) / 3)
logger.debug(f"row_height: {row_height} | col_width: {col_width}")
# Calculate spacings for better centering
spacing_top = int((im_width % col_width) / 2)
spacing_left = int((im_height % row_height) / 2)
# Define sizes for weather icons
icon_small = int(col_width / 3)
icon_medium = icon_small * 2
icon_large = icon_small * 3
# Calculate the x-axis position of each col
col1 = spacing_top
col2 = col1 + col_width
col3 = col2 + col_width
col4 = col3 + col_width
col5 = col4 + col_width
col6 = col5 + col_width
col7 = col6 + col_width
# Calculate the y-axis position of each row
line_gap = int((im_height - spacing_top - 3 * row_height) // 4)
row1 = line_gap
row2 = row1 + line_gap + row_height
row3 = row2 + line_gap + row_height
# Draw lines on each row and border
############################################################################
## draw = ImageDraw.Draw(im_black)
## draw.line((0, 0, im_width, 0), fill='red')
## draw.line((0, im_height-1, im_width, im_height-1), fill='red')
## draw.line((0, row1, im_width, row1), fill='black')
## draw.line((0, row1+row_height, im_width, row1+row_height), fill='black')
## draw.line((0, row2, im_width, row2), fill='black')
## draw.line((0, row2+row_height, im_width, row2+row_height), fill='black')
## draw.line((0, row3, im_width, row3), fill='black')
## draw.line((0, row3+row_height, im_width, row3+row_height), fill='black')
############################################################################
# Positions for current weather details
weather_icon_pos = (col1, 0)
temperature_icon_pos = (col2, row1)
temperature_pos = (col2 + icon_small, row1)
humidity_icon_pos = (col2, row2)
humidity_pos = (col2 + icon_small, row2)
windspeed_icon_pos = (col2, row3)
windspeed_pos = (col2 + icon_small, row3)
# Positions for sunrise, sunset, moonphase
moonphase_pos = (col3, row1)
sunrise_icon_pos = (col3, row2)
sunrise_time_pos = (col3 + icon_small, row2)
sunset_icon_pos = (col3, row3)
sunset_time_pos = (col3 + icon_small, row3)
# Positions for forecast 1
stamp_fc1 = (col4, row1)
icon_fc1 = (col4, row1 + row_height)
temp_fc1 = (col4, row3)
# Positions for forecast 2
stamp_fc2 = (col5, row1)
icon_fc2 = (col5, row1 + row_height)
temp_fc2 = (col5, row3)
# Positions for forecast 3
stamp_fc3 = (col6, row1)
icon_fc3 = (col6, row1 + row_height)
temp_fc3 = (col6, row3)
# Positions for forecast 4
stamp_fc4 = (col7, row1)
icon_fc4 = (col7, row1 + row_height)
temp_fc4 = (col7, row3)
# Create current-weather and weather-forecast objects
if self.location.isdigit():
logging.debug('looking up location by ID')
weather = self.owm.weather_at_id(int(self.location)).weather
forecast = self.owm.forecast_at_id(int(self.location), '3h')
else:
logging.debug('looking up location by string')
weather = self.owm.weather_at_place(self.location).weather
forecast = self.owm.forecast_at_place(self.location, '3h')
# Set decimals
dec_temp = None if self.round_temperature == True else 1
dec_wind = None if self.round_windspeed == True else 1
# Set correct temperature units
if self.units == 'metric':
temp_unit = 'celsius'
elif self.units == 'imperial':
temp_unit = 'fahrenheit'
logging.debug(f'temperature unit: {temp_unit}')
logging.debug(f'decimals temperature: {dec_temp} | decimals wind: {dec_wind}')
# Get current time
now = arrow.utcnow()
if self.forecast_interval == 'hourly':
logger.debug("getting hourly forecasts")
# Forecasts are provided for every 3rd full hour
# find out how many hours there are until the next 3rd full hour
if (now.hour % 3) != 0:
hour_gap = 3 - (now.hour % 3)
else:
hour_gap = 3
# Create timings for hourly forcasts
forecast_timings = [now.shift(hours=+ hour_gap + _).floor('hour')
for _ in range(0, 12, 3)]
# Create forecast objects for given timings
forecasts = [forecast.get_weather_at(forecast_time.datetime) for
forecast_time in forecast_timings]
# Add forecast-data to fc_data dictionary
fc_data = {}
for forecast in forecasts:
temp = '{}°'.format(round(
forecast.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
icon = forecast.weather_icon_name
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': temp,
'icon': icon,
'stamp': forecast_timings[forecasts.index(forecast)].to(
get_system_tz()).format('H.00' if self.hour_format == 24 else 'h a')
}
elif self.forecast_interval == 'daily':
logger.debug("getting daily forecasts")
def calculate_forecast(days_from_today):
"""Get temperature range and most frequent icon code for forecast
days_from_today should be int from 1-4: e.g. 2 -> 2 days from today
"""
# Create a list containing time-objects for every 3rd hour of the day
time_range = list(arrow.Arrow.range('hour',
now.shift(days=days_from_today).floor('day'),
now.shift(days=days_from_today).ceil('day')
))[::3]
# Get forecasts for each time-object
forecasts = [forecast.get_weather_at(_.datetime) for _ in time_range]
# Get all temperatures for this day
daily_temp = [round(_.temperature(unit=temp_unit)['temp'],
ndigits=dec_temp) for _ in forecasts]
# Calculate min. and max. temp for this day
temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°'
# Get all weather icon codes for this day
daily_icons = [_.weather_icon_name for _ in forecasts]
# Find most common element from all weather icon codes
status = max(set(daily_icons), key=daily_icons.count)
weekday = now.shift(days=days_from_today).format('ddd', locale=
self.locale)
return {'temp': temp_range, 'icon': status, 'stamp': weekday}
forecasts = [calculate_forecast(days) for days in range(1, 5)]
fc_data = {}
for forecast in forecasts:
fc_data['fc' + str(forecasts.index(forecast) + 1)] = {
'temp': forecast['temp'],
'icon': forecast['icon'],
'stamp': forecast['stamp']
}
for key, val in fc_data.items():
logger.debug((key, val))
# Get some current weather details
temperature = '{}°'.format(round(
weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
weather_icon = weather.weather_icon_name
humidity = str(weather.humidity)
sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone)
sunset_raw = arrow.get(weather.sunset_time()).to(self.timezone)
for key,val in fc_data.items():
logger.debug((key,val))
logger.debug(f'weather_icon: {weather_icon}')
# Get some current weather details
temperature = '{}°'.format(round(
weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp))
if self.hour_format == 12:
logger.debug('using 12 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('h:mm a')
sunset = sunset_raw.format('h:mm a')
weather_icon = weather.weather_icon_name
humidity = str(weather.humidity)
sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone)
sunset_raw = arrow.get(weather.sunset_time()).to(self.timezone)
elif self.hour_format == 24:
logger.debug('using 24 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('H:mm')
sunset = sunset_raw.format('H:mm')
logger.debug(f'weather_icon: {weather_icon}')
# Format the windspeed to user preference
if self.use_beaufort == True:
logger.debug("using beaufort for wind")
wind = str(weather.wind(unit='beaufort')['speed'])
elif self.use_beaufort == False:
if self.units == 'metric':
logging.debug('getting windspeed in metric unit')
wind = str(weather.wind(unit='meters_sec')['speed']) + 'm/s'
if self.hour_format == 12:
logger.debug('using 12 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('h:mm a')
sunset = sunset_raw.format('h:mm a')
elif self.units == 'imperial':
logging.debug('getting windspeed in imperial unit')
wind = str(weather.wind(unit='miles_hour')['speed']) + 'miles/h'
elif self.hour_format == 24:
logger.debug('using 24 hour format for sunrise/sunset')
sunrise = sunrise_raw.format('H:mm')
sunset = sunset_raw.format('H:mm')
dec = decimal.Decimal
moonphase = get_moon_phase()
# Format the windspeed to user preference
if self.use_beaufort == True:
logger.debug("using beaufort for wind")
wind = str(weather.wind(unit='beaufort')['speed'])
# Fill weather details in col 1 (current weather icon)
draw_icon(im_colour, weather_icon_pos, (col_width, im_height),
weathericons[weather_icon])
elif self.use_beaufort == False:
# Fill weather details in col 2 (temp, humidity, wind)
draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height),
'\uf053')
if self.units == 'metric':
logging.debug('getting windspeed in metric unit')
wind = str(weather.wind(unit='meters_sec')['speed']) + 'm/s'
if is_negative(temperature):
write(im_black, temperature_pos, (col_width - icon_small, row_height),
temperature, font=self.font)
else:
write(im_black, temperature_pos, (col_width - icon_small, row_height),
temperature, font=self.font)
elif self.units == 'imperial':
logging.debug('getting windspeed in imperial unit')
wind = str(weather.wind(unit='miles_hour')['speed']) + 'miles/h'
draw_icon(im_colour, humidity_icon_pos, (icon_small, row_height),
'\uf07a')
dec = decimal.Decimal
moonphase = get_moon_phase()
write(im_black, humidity_pos, (col_width - icon_small, row_height),
humidity + '%', font=self.font)
# Fill weather details in col 1 (current weather icon)
draw_icon(im_colour, weather_icon_pos, (col_width, im_height),
weathericons[weather_icon])
draw_icon(im_colour, windspeed_icon_pos, (icon_small, icon_small),
'\uf050')
# Fill weather details in col 2 (temp, humidity, wind)
draw_icon(im_colour, temperature_icon_pos, (icon_small, row_height),
'\uf053')
write(im_black, windspeed_pos, (col_width - icon_small, row_height),
wind, font=self.font)
if is_negative(temperature):
write(im_black, temperature_pos, (col_width-icon_small, row_height),
temperature, font = self.font)
else:
write(im_black, temperature_pos, (col_width-icon_small, row_height),
temperature, font = self.font)
# Fill weather details in col 3 (moonphase, sunrise, sunset)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moonphase)
draw_icon(im_colour, humidity_icon_pos, (icon_small, row_height),
'\uf07a')
draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051')
write(im_black, sunrise_time_pos, (col_width - icon_small, row_height),
sunrise, font=self.font)
write(im_black, humidity_pos, (col_width-icon_small, row_height),
humidity+'%', font = self.font)
draw_icon(im_colour, sunset_icon_pos, (icon_small, icon_small), '\uf052')
write(im_black, sunset_time_pos, (col_width - icon_small, row_height), sunset,
font=self.font)
draw_icon(im_colour, windspeed_icon_pos, (icon_small, icon_small),
'\uf050')
# Add the forecast data to the correct places
for pos in range(1, len(fc_data) + 1):
stamp = fc_data[f'fc{pos}']['stamp']
write(im_black, windspeed_pos, (col_width-icon_small, row_height),
wind, font=self.font)
icon = weathericons[fc_data[f'fc{pos}']['icon']]
temp = fc_data[f'fc{pos}']['temp']
# Fill weather details in col 3 (moonphase, sunrise, sunset)
draw_icon(im_colour, moonphase_pos, (col_width, row_height), moonphase)
write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height),
stamp, font=self.font)
draw_icon(im_colour, eval(f'icon_fc{pos}'), (col_width, row_height + line_gap * 2),
icon)
write(im_black, eval(f'temp_fc{pos}'), (col_width, row_height),
temp, font=self.font)
draw_icon(im_colour, sunrise_icon_pos, (icon_small, icon_small), '\uf051')
write(im_black, sunrise_time_pos, (col_width-icon_small, row_height),
sunrise, font = self.font)
border_h = row3 + row_height
border_w = col_width - 3 # leave 3 pixels gap
draw_icon(im_colour, sunset_icon_pos, (icon_small, icon_small), '\uf052')
write(im_black, sunset_time_pos, (col_width-icon_small, row_height), sunset,
font = self.font)
# Add borders around each sub-section
draw_border(im_black, (col1, row1), (col_width * 3 - 3, border_h),
shrinkage=(0, 0))
# Add the forecast data to the correct places
for pos in range(1, len(fc_data)+1):
stamp = fc_data[f'fc{pos}']['stamp']
for _ in range(4, 8):
draw_border(im_black, (eval(f'col{_}'), row1), (border_w, border_h),
shrinkage=(0, 0))
icon = weathericons[fc_data[f'fc{pos}']['icon']]
temp = fc_data[f'fc{pos}']['temp']
# return the images ready for the display
return im_black, im_colour
write(im_black, eval(f'stamp_fc{pos}'), (col_width, row_height),
stamp, font = self.font)
draw_icon(im_colour, eval(f'icon_fc{pos}'), (col_width, row_height+line_gap*2),
icon)
write(im_black, eval(f'temp_fc{pos}'), (col_width, row_height),
temp, font = self.font)
border_h = row3 + row_height
border_w = col_width - 3 #leave 3 pixels gap
# Add borders around each sub-section
draw_border(im_black, (col1, row1), (col_width*3 - 3, border_h),
shrinkage=(0,0))
for _ in range(4,8):
draw_border(im_black, (eval(f'col{_}'), row1), (border_w, border_h),
shrinkage=(0,0))
# return the images ready for the display
return im_black, im_colour
if __name__ == '__main__':
print(f'running {filename} in standalone mode')
print(f'running {filename} in standalone mode')

View File

@@ -1,92 +1,92 @@
#!python3
import abc
from inkycal.custom import *
class inkycal_module(metaclass=abc.ABCMeta):
"""Generic base class for inkycal modules"""
"""Generic base class for inkycal modules"""
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'generate_image') and
callable(subclass.generate_image) or
NotImplemented)
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'generate_image') and
callable(subclass.generate_image) or
NotImplemented)
def __init__(self, config):
"""Initialize module with given config"""
def __init__(self, config):
"""Initialize module with given config"""
# Initializes base module
# sets properties shared amongst all sections
self.config = conf = config['config']
self.width, self.height = conf['size']
# Initializes base module
# sets properties shared amongst all sections
self.config = conf = config['config']
self.width, self.height = conf['size']
self.padding_left = self.padding_right = conf["padding_x"]
self.padding_top = self.padding_bottom = conf['padding_y']
self.padding_left = self.padding_right = conf["padding_x"]
self.padding_top = self.padding_bottom = conf['padding_y']
self.fontsize = conf["fontsize"]
self.font = ImageFont.truetype(
fonts['NotoSansUI-Regular'], size = self.fontsize)
self.fontsize = conf["fontsize"]
self.font = ImageFont.truetype(
fonts['NotoSansUI-Regular'], size=self.fontsize)
def set(self, help=False, **kwargs):
"""Set attributes of class, e.g. class.set(key=value)
see that can be changed by setting help to True
"""
lst = dir(self).copy()
options = [_ for _ in lst if not _.startswith('_')]
if 'logger' in options: options.remove('logger')
def set(self, help=False, **kwargs):
"""Set attributes of class, e.g. class.set(key=value)
see that can be changed by setting help to True
"""
lst = dir(self).copy()
options = [_ for _ in lst if not _.startswith('_')]
if 'logger' in options: options.remove('logger')
if help == True:
print('The following can be configured:')
print(options)
if help == True:
print('The following can be configured:')
print(options)
for key, value in kwargs.items():
if key in options:
if key == 'fontsize':
self.font = ImageFont.truetype(self.font.path, value)
self.fontsize = value
else:
setattr(self, key, value)
print(f"set '{key}' to '{value}'")
else:
print(f'{key} does not exist')
pass
for key, value in kwargs.items():
if key in options:
if key == 'fontsize':
self.font = ImageFont.truetype(self.font.path, value)
self.fontsize = value
else:
setattr(self, key, value)
print(f"set '{key}' to '{value}'")
else:
print(f'{key} does not exist')
pass
# Check if validation has been implemented
try:
self._validate()
except AttributeError:
print('no validation implemented')
# Check if validation has been implemented
try:
self._validate()
except AttributeError:
print('no validation implemented')
@abc.abstractmethod
def generate_image(self):
# Generate image for this module with specified parameters
raise NotImplementedError(
'The developers were too lazy to implement this function')
@abc.abstractmethod
def generate_image(self):
# Generate image for this module with specified parameters
raise NotImplementedError(
'The developers were too lazy to implement this function')
@classmethod
def get_config(cls):
# Do not change
# Get the config of this module for the web-ui
try:
if hasattr(cls, 'requires'):
for each in cls.requires:
if not "label" in cls.requires[each]:
raise Exception(f"no label found for {each}")
if hasattr(cls, 'optional'):
for each in cls.optional:
if not "label" in cls.optional[each]:
raise Exception(f"no label found for {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')
@classmethod
def get_config(cls):
# Do not change
# Get the config of this module for the web-ui
try:
if hasattr(cls, 'requires'):
for each in cls.requires:
if not "label" in cls.requires[each]:
raise Exception(f"no label found for {each}")
if hasattr(cls, 'optional'):
for each in cls.optional:
if not "label" in cls.optional[each]:
raise Exception(f"no label found for {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

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
iCalendar parser test (ical_parser)
@@ -14,45 +13,45 @@ from urllib.request import urlopen
from inkycal.modules.ical_parser import iCalendar
from helper_functions import *
ical = iCalendar()
test_ical = 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics'
class ical_parser_test(unittest.TestCase):
def test_load_url(self):
print('testing loading via URL...', end="")
ical.load_url(test_ical)
print('OK')
def test_load_url(self):
print('testing loading via URL...', end="")
ical.load_url(test_ical)
print('OK')
def test_get_events(self):
print('testing parsing of events...', end="")
ical.get_events(arrow.now(), arrow.now().shift(weeks=30))
print('OK')
def test_get_events(self):
print('testing parsing of events...', end="")
ical.get_events(arrow.now(), arrow.now().shift(weeks=30))
print('OK')
def test_sorting(self):
print('testing sorting of events...', end="")
ical.sort()
print('OK')
def test_sorting(self):
print('testing sorting of events...', end="")
ical.sort()
print('OK')
def test_show_events(self):
print('testing if events can be shown...', end="")
ical.show_events()
print('OK')
def test_show_events(self):
print('testing if events can be shown...', end="")
ical.show_events()
print('OK')
def test_laod_from_file(self):
print('testing loading from file...', end="")
dummy = str(urlopen(test_ical).read().decode())
with open('dummy.ical', mode="w") as file:
file.write(dummy)
ical.load_from_file('dummy.ical')
print('OK')
os.remove('dummy.ical')
def test_laod_from_file(self):
print('testing loading from file...', end="")
dummy = str(urlopen(test_ical).read().decode())
with open('dummy.ical', mode="w") as file:
file.write(dummy)
ical.load_from_file('dummy.ical')
print('OK')
os.remove('dummy.ical')
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
unittest.main()

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Agenda test (inkycal_agenda)
Copyright by aceisace
@@ -7,80 +7,80 @@ Copyright by aceisace
import unittest
from inkycal.modules import Agenda as Module
from helper_functions import *
environment = get_environment()
# Set to True to preview images. Only works on Raspberry Pi OS with Desktop
use_preview = False
sample_url = "https://www.officeholidays.com/ics-fed/usa"
tests = [
{
"name": "Agenda",
"config": {
"size": [400, 200],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Agenda",
"config": {
"size": [500, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Agenda",
"config": {
"size": [300, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Agenda",
"config": {
"size": [400, 200],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"name": "Agenda",
"config": {
"size": [500, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
{
"position": 1,
"name": "Agenda",
"config": {
"size": [300, 800],
"ical_urls": sample_url,
"ical_files": None,
"date_format": "ddd D MMM",
"time_format": "HH:mm",
"padding_x": 10,
"padding_y": 10,
"fontsize": 12,
"language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end = "")
Module.get_config()
print('OK')
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test)+1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
unittest.main()

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Calendar test (inkycal_calendar)
@@ -9,84 +8,85 @@ Copyright by aceisace
import unittest
from inkycal.modules import Calendar as Module
from helper_functions import *
environment = get_environment()
# Set to True to preview images. Only works on Raspberry Pi OS with Desktop
use_preview = False
sample_url = "https://www.officeholidays.com/ics-fed/usa"
tests = [
{
"name": "Calendar",
"config": {
"size": [500, 500],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10,"padding_y": 10,"fontsize": 12,"language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Sunday",
"show_events": True,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10,"padding_y": 10,"fontsize": 12,"language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Monday",
"show_events": False,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10,"padding_y": 10,"fontsize": 12,"language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": None,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10,"padding_y": 10,"fontsize": 12,"language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [500, 500],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Sunday",
"show_events": True,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Monday",
"show_events": False,
"ical_urls": sample_url,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Calendar",
"config": {
"size": [400, 800],
"week_starts_on": "Monday",
"show_events": True,
"ical_urls": None,
"ical_files": None,
"date_format": "D MMM", "time_format": "HH:mm",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end = "")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test)+1} generating image..', end="")
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..', end="")
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
unittest.main()

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Feeds test (inykcal_feeds)
@@ -9,61 +8,62 @@ Copyright by aceisace
import unittest
from inkycal.modules import Feeds as Module
from helper_functions import *
environment = get_environment()
# Set to True to preview images. Only works on Raspberry Pi OS with Desktop
use_preview = False
tests = [
{
"name": "Feeds",
"config": {
"size": [400,200],
"feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#",
"shuffle_feeds": True,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400,100],
"feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#",
"shuffle_feeds": False,
"padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400,100],
"feed_urls": "https://www.anekdot.ru/rss/export_top.xml",
"shuffle_feeds": False,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400, 200],
"feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#",
"shuffle_feeds": True,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400, 100],
"feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#",
"shuffle_feeds": False,
"padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "en"
}
},
{
"name": "Feeds",
"config": {
"size": [400, 100],
"feed_urls": "https://www.anekdot.ru/rss/export_top.xml",
"shuffle_feeds": False,
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end = "")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test)+1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
unittest.main()

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Image test (inkycal_image)
@@ -10,113 +9,114 @@ import unittest
from inkycal.modules import Inkyimage as Module
from inkycal.custom import top_level
from helper_functions import *
environment = get_environment()
# Set to True to preview images. Only works on Raspberry Pi OS with Desktop
use_preview = False
test_path = f'{top_level}/Gallery/coffee.png'
tests = [
{
"name": "Inkyimage",
"config": {
"size": [400,200],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [800,500],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400,100],
"path": test_path,
"palette": "bw",
"autoflip": False,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400,100],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400,100],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "horizontal",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bw",
"autoflip": True,
"orientation": "vertical",
"padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 20, "padding_y": 20, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 200],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [800, 500],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bw",
"autoflip": False,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [400, 100],
"path": test_path,
"palette": "bwy",
"autoflip": True,
"orientation": "horizontal",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bw",
"autoflip": True,
"orientation": "vertical",
"padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en"
}
},
{
"name": "Inkyimage",
"config": {
"size": [500, 800],
"path": test_path,
"palette": "bwr",
"autoflip": True,
"orientation": "vertical",
"padding_x": 20, "padding_y": 20, "fontsize": 12, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end = "")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test)+1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
unittest.main()

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Jokes test (inkycal_jokes)

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Slideshow test (inkycal_slideshow)

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Stocks test (inkycal_stocks)
@@ -9,82 +8,83 @@ Copyright by aceisace
import unittest
from inkycal.modules import Stocks as Module
from helper_functions import *
environment = get_environment()
# Set to True to preview images. Only works on Raspberry Pi OS with Desktop
use_preview = False
tests = [
{
"name": "Stocks",
"config": {
"size": [528, 30],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 50],
"tickers": [],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 200],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 800],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 100],
"tickers": "TSLA,AMD,NVDA,^DJI,BTC-USD,EURUSD=X",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 400],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 30],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 50],
"tickers": [],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 200],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 800],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 100],
"tickers": "TSLA,AMD,NVDA,^DJI,BTC-USD,EURUSD=X",
"padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en"
}
},
{
"name": "Stocks",
"config": {
"size": [528, 400],
"tickers": ['TSLA', 'AMD', 'NVDA', '^DJI', 'BTC-USD', 'EURUSD=X'],
"padding_x": 10, "padding_y": 10, "fontsize": 14, "language": "en"
}
},
]
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end = "")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test)+1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
class module_test(unittest.TestCase):
def test_get_config(self):
print('getting data for web-ui...', end="")
Module.get_config()
print('OK')
def test_generate_image(self):
for test in tests:
print(f'test {tests.index(test) + 1} generating image..')
module = Module(test)
im_black, im_colour = module.generate_image()
print('OK')
if use_preview == True and environment == 'Raspberry':
preview(merge(im_black, im_colour))
if __name__ == '__main__':
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
unittest.main()
unittest.main()

View File

@@ -1,2 +0,0 @@
import unittest
from inkycal.modules import Todoist as Module

View File

@@ -1,3 +1,5 @@
#!python3
import unittest
from inkycal.modules import Todoist as Module
from helper_functions import *

View File

@@ -1,3 +1,5 @@
#!python3
import unittest
from inkycal.modules import Weather as Module
from helper_functions import *

View File

@@ -1,5 +1,4 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#!python3
"""
Main test (main)

View File

@@ -1,5 +1,4 @@
#!python3
# -*- coding: utf-8 -*-
from setuptools import setup
from os import path