diff --git a/inkycal/__init__.py b/inkycal/__init__.py index 91d44d5..f642561 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -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 diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index 633cde8..b07830b 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -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) diff --git a/inkycal/display/drivers/9_in_7.py b/inkycal/display/drivers/9_in_7.py index c4642ce..22c63bd 100644 --- a/inkycal/display/drivers/9_in_7.py +++ b/inkycal/display/drivers/9_in_7.py @@ -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 diff --git a/inkycal/display/drivers/epd_4_in_2.py b/inkycal/display/drivers/epd_4_in_2.py index 760b9de..4d82448 100644 --- a/inkycal/display/drivers/epd_4_in_2.py +++ b/inkycal/display/drivers/epd_4_in_2.py @@ -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 ### diff --git a/inkycal/display/drivers/epd_4_in_2_colour.py b/inkycal/display/drivers/epd_4_in_2_colour.py index 10e7c8b..0838618 100644 --- a/inkycal/display/drivers/epd_4_in_2_colour.py +++ b/inkycal/display/drivers/epd_4_in_2_colour.py @@ -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 ### - diff --git a/inkycal/display/drivers/epd_5_in_83.py b/inkycal/display/drivers/epd_5_in_83.py index 154a40c..e92371f 100644 --- a/inkycal/display/drivers/epd_5_in_83.py +++ b/inkycal/display/drivers/epd_5_in_83.py @@ -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 ### diff --git a/inkycal/display/drivers/epd_5_in_83_colour.py b/inkycal/display/drivers/epd_5_in_83_colour.py index 650d2e9..4ace890 100644 --- a/inkycal/display/drivers/epd_5_in_83_colour.py +++ b/inkycal/display/drivers/epd_5_in_83_colour.py @@ -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 ### - diff --git a/inkycal/display/drivers/epd_7_in_5.py b/inkycal/display/drivers/epd_7_in_5.py index 5b4ac2e..b360aa1 100644 --- a/inkycal/display/drivers/epd_7_in_5.py +++ b/inkycal/display/drivers/epd_7_in_5.py @@ -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 ### - diff --git a/inkycal/display/drivers/epd_7_in_5_colour.py b/inkycal/display/drivers/epd_7_in_5_colour.py index 7b609b8..3723c12 100644 --- a/inkycal/display/drivers/epd_7_in_5_colour.py +++ b/inkycal/display/drivers/epd_7_in_5_colour.py @@ -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 ### - diff --git a/inkycal/display/drivers/epd_7_in_5_v2.py b/inkycal/display/drivers/epd_7_in_5_v2.py index aba0660..97c6f0e 100644 --- a/inkycal/display/drivers/epd_7_in_5_v2.py +++ b/inkycal/display/drivers/epd_7_in_5_v2.py @@ -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 ### - diff --git a/inkycal/display/drivers/epd_7_in_5_v2_colour.py b/inkycal/display/drivers/epd_7_in_5_v2_colour.py index 5cc3cc6..bb732b3 100644 --- a/inkycal/display/drivers/epd_7_in_5_v2_colour.py +++ b/inkycal/display/drivers/epd_7_in_5_v2_colour.py @@ -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 ### - diff --git a/inkycal/display/drivers/epd_7_in_5_v3.py b/inkycal/display/drivers/epd_7_in_5_v3.py index 69ae29b..363ad4d 100644 --- a/inkycal/display/drivers/epd_7_in_5_v3.py +++ b/inkycal/display/drivers/epd_7_in_5_v3.py @@ -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 ### diff --git a/inkycal/display/drivers/epd_7_in_5_v3_colour.py b/inkycal/display/drivers/epd_7_in_5_v3_colour.py index f85eb99..f808794 100644 --- a/inkycal/display/drivers/epd_7_in_5_v3_colour.py +++ b/inkycal/display/drivers/epd_7_in_5_v3_colour.py @@ -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 ### diff --git a/inkycal/display/drivers/epdconfig.py b/inkycal/display/drivers/epdconfig.py index 0e2fd7b..c8642f4 100644 --- a/inkycal/display/drivers/epdconfig.py +++ b/inkycal/display/drivers/epdconfig.py @@ -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 ### diff --git a/inkycal/modules/__init__.py b/inkycal/modules/__init__.py index 7ea2de5..d97106e 100644 --- a/inkycal/modules/__init__.py +++ b/inkycal/modules/__init__.py @@ -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 diff --git a/inkycal/modules/dev_module.py b/inkycal/modules/dev_module.py index 97a4d6b..dd11731 100644 --- a/inkycal/modules/dev_module.py +++ b/inkycal/modules/dev_module.py @@ -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) - diff --git a/inkycal/modules/ical_parser.py b/inkycal/modules/ical_parser.py index 5b9349a..2698d49 100644 --- a/inkycal/modules/ical_parser.py +++ b/inkycal/modules/ical_parser.py @@ -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') diff --git a/inkycal/modules/inky_image.py b/inkycal/modules/inky_image.py index b04859f..e78a0ec 100644 --- a/inkycal/modules/inky_image.py +++ b/inkycal/modules/inky_image.py @@ -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') diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 87fe6b8..46b4025 100644 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -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') diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index d8e2d42..b37b5ea 100644 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -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') diff --git a/inkycal/modules/inkycal_feeds.py b/inkycal/modules/inkycal_feeds.py index 3a700bd..5a53105 100644 --- a/inkycal/modules/inkycal_feeds.py +++ b/inkycal/modules/inkycal_feeds.py @@ -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') diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index 20ebd07..3a1b659 100644 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -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') diff --git a/inkycal/modules/inkycal_jokes.py b/inkycal/modules/inkycal_jokes.py index d653893..db69a6f 100644 --- a/inkycal/modules/inkycal_jokes.py +++ b/inkycal/modules/inkycal_jokes.py @@ -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') diff --git a/inkycal/modules/inkycal_server.py b/inkycal/modules/inkycal_server.py index d6ba2a0..b650d18 100644 --- a/inkycal/modules/inkycal_server.py +++ b/inkycal/modules/inkycal_server.py @@ -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 - diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index ee1ec1d..2ad03b5 100644 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -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') diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index 2d623e9..c354b6e 100644 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -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') diff --git a/inkycal/modules/inkycal_todoist.py b/inkycal/modules/inkycal_todoist.py index 27d4fe7..d2c921b 100644 --- a/inkycal/modules/inkycal_todoist.py +++ b/inkycal/modules/inkycal_todoist.py @@ -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') diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 25a102c..1c26dcb 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -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') diff --git a/inkycal/modules/template.py b/inkycal/modules/template.py index eaf0448..7172960 100644 --- a/inkycal/modules/template.py +++ b/inkycal/modules/template.py @@ -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') diff --git a/inkycal/tests/ical_parser_test.py b/inkycal/tests/ical_parser_test.py index d77115d..ebefdcf 100644 --- a/inkycal/tests/ical_parser_test.py +++ b/inkycal/tests/ical_parser_test.py @@ -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() diff --git a/inkycal/tests/inkycal_agenda_test.py b/inkycal/tests/inkycal_agenda_test.py index bd6dda4..819b444 100644 --- a/inkycal/tests/inkycal_agenda_test.py +++ b/inkycal/tests/inkycal_agenda_test.py @@ -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() diff --git a/inkycal/tests/inkycal_calendar_test.py b/inkycal/tests/inkycal_calendar_test.py index 828de1c..37f0e1f 100644 --- a/inkycal/tests/inkycal_calendar_test.py +++ b/inkycal/tests/inkycal_calendar_test.py @@ -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() diff --git a/inkycal/tests/inkycal_feeds_test.py b/inkycal/tests/inkycal_feeds_test.py index 38c0c01..1d84e2a 100644 --- a/inkycal/tests/inkycal_feeds_test.py +++ b/inkycal/tests/inkycal_feeds_test.py @@ -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() diff --git a/inkycal/tests/inkycal_image_test.py b/inkycal/tests/inkycal_image_test.py index 2ed3354..9f27821 100644 --- a/inkycal/tests/inkycal_image_test.py +++ b/inkycal/tests/inkycal_image_test.py @@ -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() diff --git a/inkycal/tests/inkycal_jokes_test.py b/inkycal/tests/inkycal_jokes_test.py index c74d5f7..756d7be 100644 --- a/inkycal/tests/inkycal_jokes_test.py +++ b/inkycal/tests/inkycal_jokes_test.py @@ -1,5 +1,4 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- +#!python3 """ Jokes test (inkycal_jokes) diff --git a/inkycal/tests/inkycal_slideshow_test.py b/inkycal/tests/inkycal_slideshow_test.py index d96e66f..68c0692 100644 --- a/inkycal/tests/inkycal_slideshow_test.py +++ b/inkycal/tests/inkycal_slideshow_test.py @@ -1,5 +1,4 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- +#!python3 """ Slideshow test (inkycal_slideshow) diff --git a/inkycal/tests/inkycal_stocks_test.py b/inkycal/tests/inkycal_stocks_test.py index 4e465b0..960c530 100644 --- a/inkycal/tests/inkycal_stocks_test.py +++ b/inkycal/tests/inkycal_stocks_test.py @@ -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() diff --git a/inkycal/tests/inkycal_todo.py b/inkycal/tests/inkycal_todo.py deleted file mode 100644 index 22045ae..0000000 --- a/inkycal/tests/inkycal_todo.py +++ /dev/null @@ -1,2 +0,0 @@ -import unittest -from inkycal.modules import Todoist as Module diff --git a/inkycal/tests/inkycal_todoist_test.py b/inkycal/tests/inkycal_todoist_test.py index 5d7294a..744c693 100644 --- a/inkycal/tests/inkycal_todoist_test.py +++ b/inkycal/tests/inkycal_todoist_test.py @@ -1,3 +1,5 @@ +#!python3 + import unittest from inkycal.modules import Todoist as Module from helper_functions import * diff --git a/inkycal/tests/inkycal_weather_test.py b/inkycal/tests/inkycal_weather_test.py index 5204e0b..ecea43d 100644 --- a/inkycal/tests/inkycal_weather_test.py +++ b/inkycal/tests/inkycal_weather_test.py @@ -1,3 +1,5 @@ +#!python3 + import unittest from inkycal.modules import Weather as Module from helper_functions import * diff --git a/inkycal/tests/main_test.py b/inkycal/tests/main_test.py index 3c0d11b..e6125af 100644 --- a/inkycal/tests/main_test.py +++ b/inkycal/tests/main_test.py @@ -1,5 +1,4 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- +#!python3 """ Main test (main) diff --git a/setup.py b/setup.py index e1e0f42..7bae03c 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!python3 -# -*- coding: utf-8 -*- from setuptools import setup from os import path