diff --git a/.gitignore.txt b/.gitignore.txt new file mode 100644 index 0000000..c29211a --- /dev/null +++ b/.gitignore.txt @@ -0,0 +1,5 @@ +*.pyc +__pycache__/ +_build +_static +_templates \ No newline at end of file diff --git a/auto-update.sh b/auto-update.sh deleted file mode 100644 index 2903b1f..0000000 --- a/auto-update.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -# Script for updating the Inky-Calendar software. This will automatically -# transfer the user's own details with the placeholders in the settings.py file - -# To-do: Delete the old settings.py file after all operations are done - -in="/home/pi/settings.py.old" -out="/home/pi/Inky-Calendar/Calendar/settings.py" - -# replace template iCalendar URLs with user-defined URLs -sed -n -e "/^ical_urls/r $in" -i -e "/^ical_urls/d" $out -sed -n -e "/^rss_feeds/r $in" -i -e "/^rss_feeds/d" $out -sed -n -e "/^update_interval/r $in" -i -e "/^update_interval/d" $out -sed -n -e "/^api_key/r $in" -i -e "/^api_key/d" $out -sed -n -e "/^location/r $in" -i -e "/^location/d" $out -sed -n -e "/^week_starts_on/r $in" -i -e "/^week_starts_on/d" $out -sed -n -e "/^calibration_hours/r $in" -i -e "/^calibration_hours/d" $out -sed -n -e "/^display_colours/r $in" -i -e "/^display_colours/d" $out -sed -n -e "/^language/r $in" -i -e "/^language/d" $out -sed -n -e "/^units/r $in" -i -e "/^units/d" $out -sed -n -e "/^hours/r $in" -i -e "/^hours/d" $out -sed -n -e "/^top_section/r $in" -i -e "/^top_section/d" $out -sed -n -e "/^middle_section/r $in" -i -e "/^middle_section/d" $out -sed -n -e "/^bottom_section/r $in" -i -e "/^bottom_section/d" $out - -echo -e 'All operations done' diff --git a/inkycal/Inkycal.py b/inkycal/Inkycal.py new file mode 100644 index 0000000..900ec58 --- /dev/null +++ b/inkycal/Inkycal.py @@ -0,0 +1,45 @@ +from importlib import import_module + +from inkycal.configuration.settings_parser import inkycal_settings as settings +from inkycal.display.layout import inkycal_layout as layout + + + +##modules = settings.which_modules() +##for module in modules: +## if module == 'inkycal_rss': +## module = import_module('inkycal.modules.'+module) +## #import_module('modules.'+module) +##print(module) + +settings_file = '/home/pi/Desktop/settings.json' + + +class inkycal: + def __init__(self, settings_file_path): + """Load settings file from path""" + # Load settings file + self.settings = settings(settings_file_path) + self.model = self.settings.model + + def create_canvas(self): + """Create a canvas with same size as the specified model""" + self.layout = layout(model=self.model) + + def create_custom_canvas(self, width=None, height=None, + supports_colour=False): + """Create a custom canvas by specifying height and width""" + self.layout = layout(model=model, width=width, height=height, + supports_colour=supports_colour) + + def create_sections(self): + """Create sections with default sizes""" + self.layout.create_sections() + + def create_custom_sections(self, top_section=0.10, middle_section=0.65, + bottom_section=0.25): + """Create custom-sized sections in the canvas""" + self.layout.create_sections(top_section=top_section, + middle_section=middle_section, + bottom_section=bottom_section) + diff --git a/inkycal/__init__.py b/inkycal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inkycal/configuration/__init__.py b/inkycal/configuration/__init__.py new file mode 100644 index 0000000..b4acbf5 --- /dev/null +++ b/inkycal/configuration/__init__.py @@ -0,0 +1,2 @@ +from .settings_parser import inkycal_settings +print('loaded settings') diff --git a/inkycal/configuration/settings.json b/inkycal/configuration/settings.json new file mode 100644 index 0000000..8af98b2 --- /dev/null +++ b/inkycal/configuration/settings.json @@ -0,0 +1,42 @@ +{ + "language": "en", + "units": "metric", + "hours": 24, + "model": "epd_7_in_5_v2_colour", + "update_interval": 60, + "calibration_hours": [ + 0, + 12, + 18 + ], + "display_orientation": "normal", + "panels": [ + { + "location": "top", + "type": "inkycal_weather", + "config": { + "api_key": "topsecret", + "location": "Stuttgart, DE" + } + }, + { + "location": "middle", + "type": "inkycal_calendar", + "config": { + "week_starts_on": "Monday", + "ical_urls": [ + "https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics" + ] + } + }, + { + "location": "bottom", + "type": "inkycal_rss", + "config": { + "rss_urls": [ + "http://feeds.bbci.co.uk/news/world/rss.xml#" + ] + } + } + ] +} diff --git a/inkycal/configuration/settings_parser.py b/inkycal/configuration/settings_parser.py new file mode 100644 index 0000000..5318ee0 --- /dev/null +++ b/inkycal/configuration/settings_parser.py @@ -0,0 +1,137 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Json settings parser. Currently in alpha! +Copyright by aceisace +""" + +import json +from os import chdir #Ad-hoc + +# TODO: +# Check of jsmin can/should be used to parse jsonc settings file +# Remove check of fixed settings file location. Ask user to specify path +# to settings file + +from os import path + +class inkycal_settings: + """Load and validate settings from the settings file""" + + __supported_languages = ['en', 'de', 'ru', 'it', 'es', 'fr', 'el', 'sv', 'nl', + 'pl', 'ua', 'nb', 'vi', 'zh_tw', 'zh-cn', 'ja', 'ko'] + __supported_units = ['metric', 'imperial'] + __supported_hours = [12, 24] + __supported_display_orientation = ['normal', 'upside_down'] + __supported_models = [ + 'epd_7_in_5_v2_colour', 'epd_7_in_5_v2', + 'epd_7_in_5_colour', 'epd_7_in_5', + 'epd_5_in_83_colour','epd_5_in_83', + 'epd_4_in_2_colour', 'epd_4_in_2' + ] + + def __init__(self, settings_file_path): + """Load settings from path (folder or settings.json file)""" + try: + if settings_file_path.endswith('settings.json'): + folder = settings_file_path.split('/settings.json')[0] + else: + folder = settings_file_path + + chdir(folder) + with open("settings.json") as file: + self.raw_settings = json.load(file) + + except FileNotFoundError: + print('No settings file found in specified location') + + try: + self.language = self.raw_settings['language'] + if self.language not in self.__supported_languages or type(self.language) != str: + print('Unsupported language: {}!. Switching to english'.format(language)) + self.language = 'en' + + + self.units = self.raw_settings['units'] + if self.units not in self.__supported_units or type(self.units) != str: + print('Units ({}) not supported, using metric units.'.format(units)) + self.units = 'metric' + + + self.hours = self.raw_settings['hours'] + if self.hours not in self.__supported_hours or type(self.hours) != int: + print('Selected hours: {} not supported, using 24-hours'.format(hours)) + self.hours = '24' + + + self.model = self.raw_settings['model'] + if self.model not in self.__supported_models or type(self.model) != str: + print('Model: {} not supported. Please select a valid option'.format(model)) + print('Switching to 7.5" ePaper black-white (v1) (fallback)') + self.model = 'epd_7_in_5' + + + self.calibration_hours = self.raw_settings['calibration_hours'] + if not self.calibration_hours or type(self.calibration_hours) != list: + print('Invalid calibration hours: {}'.format(calibration_hours)) + print('Using default option, 0am,12am,6pm') + self.calibration_hours = [0,12,18] + + + self.display_orientation = self.raw_settings['display_orientation'] + if self.display_orientation not in self.__supported_display_orientation or type( + self.display_orientation) != str: + print('Invalid ({}) display orientation.'.format(display_orientation)) + print('Switching to default orientation, normal-mode') + self.display_orientation = 'normal' + + ### Check if empty, If empty, set to none + for sections in self.raw_settings['panels']: + + if sections['location'] == 'top': + self.top_section = sections['type'] + self.top_section_config = sections['config'] + + elif sections['location'] == 'middle': + self.middle_section = sections['type'] + self.middle_section_config = sections['config'] + + elif sections['location'] == 'bottom': + self.bottom_section = sections['type'] + self.bottom_section_config = sections['config'] + + + print('settings loaded') + except Exception as e: + print(e.reason) + + def module_init(self, module_name): + """Get all data from settings file by providing the module name""" + if module_name == self.top_section: + config = self.top_section_config + elif module_name == self.middle_section: + config = self.middle_section_config + elif module_name == self.bottom_section: + config = self.bottom_section_config + else: + print('Invalid module name!') + config = None + + for module in self.raw_settings['panels']: + if module_name == module['type']: + location = module['location'] + + return config, location + + def which_modules(self): + """Returns a list of modules (from settings file) which should be loaded + on start""" + lst = [self.top_section, self.middle_section, self.bottom_section] + return lst + + +def main(): + print('running settings parser as standalone...') + +if __name__ == '__main__': + main() diff --git a/inkycal/display/__init__.py b/inkycal/display/__init__.py new file mode 100644 index 0000000..35b3f46 --- /dev/null +++ b/inkycal/display/__init__.py @@ -0,0 +1,2 @@ +from .layout import inkycal_layout +print('imported layout class') diff --git a/modules/drivers/init.py b/inkycal/display/drivers/__init__.py similarity index 100% rename from modules/drivers/init.py rename to inkycal/display/drivers/__init__.py diff --git a/modules/drivers/epd_4_in_2.py b/inkycal/display/drivers/epd_4_in_2.py similarity index 100% rename from modules/drivers/epd_4_in_2.py rename to inkycal/display/drivers/epd_4_in_2.py diff --git a/modules/drivers/epd_4_in_2_colour.py b/inkycal/display/drivers/epd_4_in_2_colour.py similarity index 100% rename from modules/drivers/epd_4_in_2_colour.py rename to inkycal/display/drivers/epd_4_in_2_colour.py diff --git a/modules/drivers/epd_5_in_83.py b/inkycal/display/drivers/epd_5_in_83.py similarity index 100% rename from modules/drivers/epd_5_in_83.py rename to inkycal/display/drivers/epd_5_in_83.py diff --git a/modules/drivers/epd_5_in_83_colour.py b/inkycal/display/drivers/epd_5_in_83_colour.py similarity index 100% rename from modules/drivers/epd_5_in_83_colour.py rename to inkycal/display/drivers/epd_5_in_83_colour.py diff --git a/modules/drivers/epd_7_in_5.py b/inkycal/display/drivers/epd_7_in_5.py similarity index 100% rename from modules/drivers/epd_7_in_5.py rename to inkycal/display/drivers/epd_7_in_5.py diff --git a/modules/drivers/epd_7_in_5_colour.py b/inkycal/display/drivers/epd_7_in_5_colour.py similarity index 100% rename from modules/drivers/epd_7_in_5_colour.py rename to inkycal/display/drivers/epd_7_in_5_colour.py diff --git a/modules/drivers/epd_7_in_5_v2.py b/inkycal/display/drivers/epd_7_in_5_v2.py similarity index 100% rename from modules/drivers/epd_7_in_5_v2.py rename to inkycal/display/drivers/epd_7_in_5_v2.py diff --git a/modules/drivers/epd_7_in_5_v2_colour.py b/inkycal/display/drivers/epd_7_in_5_v2_colour.py similarity index 100% rename from modules/drivers/epd_7_in_5_v2_colour.py rename to inkycal/display/drivers/epd_7_in_5_v2_colour.py diff --git a/modules/drivers/epdconfig.py b/inkycal/display/drivers/epdconfig.py similarity index 100% rename from modules/drivers/epdconfig.py rename to inkycal/display/drivers/epdconfig.py diff --git a/inkycal/display/layout.py b/inkycal/display/layout.py new file mode 100644 index 0000000..1a6ca46 --- /dev/null +++ b/inkycal/display/layout.py @@ -0,0 +1,82 @@ +class inkycal_layout: + """Page layout handling""" + + def __init__(self, model=None, width=None, height=None, + supports_colour=False): + """Initialize parameters for specified epaper model + Use model parameter to specify display OR + Crate a custom display with given width and height""" + + self.background_colour = 'white' # Move to inkycal_rendering + self.text_colour = 'black' # Move to inkycal_rendering + + if (model != None) and (width == None) and (height == None): + display_dimensions = { + 'epd_7_in_5_v2_colour': (800, 400), + 'epd_7_in_5_v2': (800, 400), + 'epd_7_in_5_colour': (640, 384), + 'epd_7_in_5': (640, 384), + 'epd_5_in_83_colour': (600, 448), + 'epd_5_in_83': (600, 448), + 'epd_4_in_2_colour': (400, 300), + 'epd_4_in_2': (400, 300), + } + + self.display_height, self.display_width = display_dimensions[model] + if 'colour' in model: + self.three_colour_support = True + + elif width and height: + self.display_height = width + self.display_width = height + self.supports_colour = supports_colour + + else: + print("Can't create a layout without given sizes") + raise + + self.__top_section_width = self.display_width + self.__middle_section_width = self.display_width + self.__bottom_section_width = self.display_width + self.create_sections() + + def create_sections(self, top_section=0.10, middle_section=0.65, + bottom_section=0.25): + """Allocate fixed percentage height for top and middle section + e.g. 0.2 = 20% (Leave empty for default values) + Set top/bottom_section to 0 to allocate more space for the middle section + """ + scale = lambda percentage: round(percentage * self.display_height) + + if top_section == 0 or bottom_section == 0: + if top_section == 0: + self.__top_section_height = 0 + + if bottom_section == 0: + self.__bottom_section_height = 0 + + self.__middle_section_height = scale(1 - top_section - bottom_section) + else: + if top_section + middle_section + bottom_section > 1.0: + print('All percentages should add up to max 100%, not more!') + raise + + self.__top_section_height = scale(top_section) + self.__middle_section_height = scale(middle_section) + self.__bottom_section_height = (self.display_height - + self.__top_section_height - self.__middle_section_height) + + def get_section_size(self, section): + """Enter top/middle/bottom to get the size of the section as a tuple: + (width, height)""" + if section not in ['top','middle','bottom']: + print('Invalid section: ', section) + raise + else: + if section == 'top': + size = (self.__top_section_width, self.__top_section_height) + elif section == 'middle': + size = (self.__middle_section_width, self.__middle_section_height) + elif section == 'bottom': + size = (self.__bottom_section_width, self.__bottom_section_height) + return size diff --git a/inkycal/display/operations.py b/inkycal/display/operations.py new file mode 100644 index 0000000..e69de29 diff --git a/inkycal/modules/__init__.py b/inkycal/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/inkycal/modules/inkycal_rss.py b/inkycal/modules/inkycal_rss.py new file mode 100644 index 0000000..763ddbc --- /dev/null +++ b/inkycal/modules/inkycal_rss.py @@ -0,0 +1,140 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +RSS module for Inky-Calendar Project +Copyright by aceisace +""" + +from inkycal.render.functions import * +from random import shuffle + +try: + import feedparser +except ImportError: + print('feedparser is not installed! Please install with:') + print('pipe install feedparser') + + +# Debug Data +size = (384, 160) +config = {'rss_urls': ['http://feeds.bbci.co.uk/news/world/rss.xml#']} + + +class inkycal_rss: + + def __init__(self, section_size, section_config): + """Initialize inkycal_rss module""" + self.name = os.path.basename(__file__).split('.py')[0] + self.config = section_config + self.width, self.height = section_size + + self.background_colour = 'white' + self.font_colour = 'black' + self.fontsize = 12 + self.font = ImageFont.truetype( + fonts['NotoSans-SemiCondensed'], size = self.fontsize) + self.padding_x = 0.02 + self.padding_y = 0.05 + print('{0} loaded'.format(self.name)) + + def set(self, **kwargs): + """Manually set some parameters of this module""" + for key, value in kwargs.items(): + if key in self.__dict__: + setattr(self, key, value) + else: + print('{0} does not exist'.format(key)) + pass + + def get(self, **kwargs): + """Manually get some parameters of this module""" + for key, value in kwargs.items(): + if key in self.__dict__: + getattr(self, key, value) + else: + print('{0} does not exist'.format(key)) + pass + + def get_options(self): + """Get all options which can be changed""" + return self.__dict__ + + def generate_image(self): + """Generate image for this module""" + + # Define new image size with respect to padding + im_width = int(self.width - (self.width * 2 * self.padding_x)) + im_height = int(self.height - (self.height * 2 * self.padding_y)) + im_size = im_width, im_height + + # Create an image for black pixels and one for coloured pixels + im_black = Image.new('RGB', size = im_size, color = self.background_colour) + im_colour = Image.new('RGB', size = im_size, color = 'white') + + # Set some parameters for formatting rss 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 line_positions + line_positions = [ + (0, spacing_top + _ * line_height ) for _ in range(max_lines)] + + if internet_available() == True: + print('Connection test passed') + else: + # write 'No network available :(' + raise Exception('Network could not be reached :(') + + try: + # Create list containing all rss-feeds from all rss-feed urls + parsed_feeds = [] + for feeds in self.config['rss_urls']: + text = feedparser.parse(feeds) + for posts in text.entries: + parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) + # print(parsed_feeds) + + # Shuffle the list to prevent showing the same content + shuffle(parsed_feeds) + + # 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 + + for posts in parsed_feeds: + wrapped = text_wrap(posts, font = self.font, max_width = line_width) + counter += len(filtered_feeds) + len(wrapped) + if counter < max_lines: + filtered_feeds.append(wrapped) + filtered_feeds = flatten(filtered_feeds) + + # Write rss-feeds on image + """Write the correctly formatted text on the display""" + for _ in range(len(filtered_feeds)): + write(im_black, line_positions[_], (line_width, line_height), + filtered_feeds[_], font = self.font, alignment= 'left') + + # Cleanup + del filtered_feeds, parsed_feeds, wrapped, counter, text + + except Exception as e: + print('Error in {0}'.format(self.name)) + print('Reason: ',e) + write(im_black, (0,0), (im_width, im_height), str(e), font = self.font) + + # Save image of black and colour channel in image-folder + im_black.save(images+self.name+'.png') + im_colour.save(images+self.name+'_colour.png') + +if __name__ == '__main__': + print('running {0} in standalone mode'.format( + os.path.basename(__file__).split('.py')[0])) diff --git a/inkycal/render/__init__.py b/inkycal/render/__init__.py new file mode 100644 index 0000000..4663002 --- /dev/null +++ b/inkycal/render/__init__.py @@ -0,0 +1 @@ +from .fonts import inkycal_fonts diff --git a/inkycal/render/functions.py b/inkycal/render/functions.py new file mode 100644 index 0000000..4594317 --- /dev/null +++ b/inkycal/render/functions.py @@ -0,0 +1,165 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Inky-Calendar custom-functions for ease-of-use + +Copyright by aceisace +""" +from PIL import Image, ImageDraw, ImageFont, ImageColor +from urllib.request import urlopen +import os +import time + + +##from glob import glob +##import importlib +##import subprocess as subp +##import numpy +##import arrow +##from pytz import timezone + + + +##"""Set some display parameters""" +##driver = importlib.import_module('drivers.'+model) + +# Get the path to the Inky-Calendar folder +top_level = os.path.dirname( + os.path.abspath(os.path.dirname(__file__))).split('/inkycal')[0] + +# Get path of 'fonts' and 'images' folders within Inky-Calendar folder +fonts_location = top_level + '/fonts/' +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) + + if filename.endswith('.ttf'): + name = filename.split('.ttf')[0] + fonts[name] = os.path.join(path, filename) + +del name, filename, files + +available_fonts = [key for key,values in fonts.items()] + +def get_fonts(): + """Print all available fonts by name""" + for fonts in available_fonts: + print(fonts) + +def write(image, xy, box_size, text, font=None, **kwargs): + """Write text on specified image + image = on which image should the text be added? + xy = xy-coordinates as tuple -> (x,y) + box_size = size of text-box -> (width,height) + text = string (what to write) + font = which font to use + """ + + allowed_kwargs = ['alignment', 'autofit', 'colour', 'rotation' + 'fill_width', 'fill_height'] + alignment='center' + autofit = False + fill_width = 1.0 + fill_height = 0.8 + colour = 'black' + rotation = None + + for key, value in kwargs.items(): + if key in allowed_kwargs: + setattr(write, key, value) + else: + print('{0} does not exist'.format(key)) + pass + + 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, 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, 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 + 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] + + # Align text to desired position + if alignment == "" or "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) + + 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): + """Split long text (text-wrapping). Returns a list""" + 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(): + """check if the internet is available""" + try: + urlopen('https://google.com',timeout=5) + return True + except URLError as err: + return False + + +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 diff --git a/logs/dummy-file.txt b/logs/dummy-file.txt deleted file mode 100644 index 98697fb..0000000 --- a/logs/dummy-file.txt +++ /dev/null @@ -1 +0,0 @@ -This is just a dummy file. diff --git a/modules/init.py b/modules/init.py deleted file mode 100644 index 9b2c532..0000000 --- a/modules/init.py +++ /dev/null @@ -1 +0,0 @@ -#nothing in here. What did you expect? \ No newline at end of file diff --git a/modules/inkycal.py b/modules/inkycal.py deleted file mode 100644 index b265503..0000000 --- a/modules/inkycal.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -v1.7.2 - -Main file of Inky-Calendar software. Creates dynamic images for each section, -assembles them and sends it to the E-Paper - -Copyright by aceisace -""" -from __future__ import print_function -from configuration import * -import arrow -from time import sleep -import gc - -"""Perepare for execution of main programm""" -calibration_countdown = 'initial' -skip_calibration = False -upside_down = False - -image_cleanup() - -try: - top_section_module = importlib.import_module(top_section) -except ValueError: - print('Something went wrong while importing the top-section module:', top_section) - pass - -try: - middle_section_module = importlib.import_module(middle_section) -except ValueError: - print('Something went wrong while importing the middle_section module', middle_section) - pass - -try: - bottom_section_module = importlib.import_module(bottom_section) -except ValueError: - print('Something went wrong while importing the bottom_section module', bottom_section) - pass - -"""Check time and calibrate display if time """ -while True: - now = arrow.now(tz=get_tz()) - for _ in range(1): - image = Image.new('RGB', (display_width, display_height), background_colour) - if three_colour_support == True: - image_col = Image.new('RGB', (display_width, display_height), 'white') - - """------------------Add short info------------------""" - print('Current Date: {0} \nCurrent Time: {1}'.format(now.format( - 'D MMM YYYY'), now.format('HH:mm'))) - print('-----------Main programm started now----------') - - """------------------Calibration check----------------""" - if skip_calibration != True: - print('Calibration..', end = ' ') - if now.hour in calibration_hours: - if calibration_countdown == 'initial': - print('required. Performing calibration now.') - calibration_countdown = 0 - calibrate_display(3) - else: - if calibration_countdown % (60 // int(update_interval)) == 0: - calibrate_display(3) - calibration_countdown = 0 - else: - print('not required. Continuing...') - else: - print('Calibration skipped!. Please note that not calibrating e-paper', - 'displays causes ghosting') - - - """----------------------top-section-image-----------------------------""" - try: - top_section_module.main() - top_section_image = Image.open(image_path + top_section+'.png') - image.paste(top_section_image, (0, 0)) - - if three_colour_support == True: - top_section_image_col = Image.open(image_path + top_section+'_col.png') - image_col.paste(top_section_image_col, (0, 0)) - - except Exception as error: - print(error) - pass - - """----------------------middle-section-image---------------------------""" - try: - middle_section_module.main() - middle_section_image = Image.open(image_path + middle_section+'.png') - image.paste(middle_section_image, (0, middle_section_offset)) - - if three_colour_support == True: - middle_section_image_col = Image.open(image_path + middle_section+'_col.png') - image_col.paste(middle_section_image_col, (0, middle_section_offset)) - - except Exception as error: - print(error) - pass - - - """----------------------bottom-section-image---------------------------""" - try: - bottom_section_module.main() - bottom_section_image = Image.open(image_path + bottom_section+'.png') - image.paste(bottom_section_image, (0, bottom_section_offset)) - - if three_colour_support == True: - bottom_section_image_col = Image.open(image_path + bottom_section+'_col.png') - image_col.paste(bottom_section_image_col, (0, bottom_section_offset)) - - except Exception as error: - print(error) - pass - - """---------------------------------------------------------------------""" - if upside_down == True: - image = image.rotate(180, expand=True) - if three_colour_support == True: - image_col = image_col.rotate(180, expand=True) - - image = optimise_colours(image) - image.save(image_path + 'canvas.png') - - if three_colour_support == True: - image_col = optimise_colours(image_col) - image_col.save(image_path+'canvas_col.png') - - """---------Refreshing E-Paper with newly created image-----------""" - epaper = driver.EPD() - print('Initialising E-Paper...', end = '') - epaper.init() - print('Done') - - if three_colour_support == True: - print('Sending image data and refreshing display...', end='') - epaper.display(epaper.getbuffer(image), epaper.getbuffer(image_col)) - print('Done') - else: - print('Sending image data and refreshing display...', end='') - epaper.display(epaper.getbuffer(image)) - print('Done') - - print('Sending E-Paper to deep sleep...', end = '') - epaper.sleep() - print('Done') - - """--------------Post processing after main loop-----------------""" - """Collect some garbage to free up some resources""" - gc.collect() - - """Adjust calibration countdowns""" - if calibration_countdown == 'initial': - calibration_countdown = 0 - calibration_countdown += 1 - - """Calculate duration until next display refresh""" - for _ in range(1): - update_timings = [(60 - int(update_interval)*updates) for updates in - range(60//int(update_interval))][::-1] - - for _ in update_timings: - if now.minute <= _: - minutes = _ - now.minute - break - - refresh_countdown = minutes*60 + (60 - now.second) - print('{0} Minutes left until next refresh'.format(minutes)) - - del update_timings, minutes, image - image_cleanup() - sleep(refresh_countdown) diff --git a/modules/inkycal_agenda.py b/modules/inkycal_agenda.py deleted file mode 100644 index e569549..0000000 --- a/modules/inkycal_agenda.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -Agenda module for Inky-Calendar Project -Copyright by aceisace -""" -from __future__ import print_function -from inkycal_icalendar import fetch_events -from configuration import* - -show_events = True -print_events = False -style = 'D MMM YY HH:mm' -all_day_str = 'All day' - -"""Add a border to increase readability""" -border_top = int(middle_section_height * 0.02) -border_left = int(middle_section_width * 0.02) - -"""Choose font optimised for the agenda section""" -font = ImageFont.truetype(NotoSans+'Medium.ttf', agenda_fontsize) -line_height = int(font.getsize('hg')[1] * 1.2) + 1 -line_width = int(middle_section_width - (border_left*2)) - -"""Set some positions for events, dates and times""" -date_col_width = int(line_width * 0.20) -time_col_width = int(line_width * 0.15) -event_col_width = int(line_width - date_col_width - time_col_width) - -date_col_start = border_left -time_col_start = date_col_start + date_col_width -event_col_start = time_col_start + time_col_width - -"""Find max number of lines that can fit in the middle section and allocate -a position for each line""" -max_lines = int(middle_section_height+bottom_section_height - - (border_top * 2))// line_height - -line_pos = [(border_left, int(top_section_height + border_top + line * line_height)) - for line in range(max_lines)] - -def generate_image(): - if middle_section == 'inkycal_agenda' and internet_available() == True: - try: - clear_image('middle_section') - if not bottom_section: - clear_image('bottom_section') - - print('Agenda module: Generating image...', end = '') - now = arrow.now(get_tz()) - today_start = arrow.get(now.year, now.month, now.day) - - """Create a list of dictionaries containing dates of the next days""" - agenda_events = [{'date':today_start.replace(days=+_), - 'date_str': now.replace(days=+_).format('ddd D MMM',locale=language), - 'type':'date'} for _ in range(max_lines)] - - """Copy the list from the icalendar module with some conditions""" - upcoming_events = fetch_events() - filtered_events = [events for events in upcoming_events if - events.end > now] - - """Set print_events_to True to print all events in this month""" - if print_events == True and filtered_events: - auto_line_width = max(len(_.name) for _ in filtered_events) - for events in filtered_events: - print('{0} {1} | {2} | {3} | All day ='.format(events.name, - ' '* (auto_line_width - len(events.name)), events.begin.format(style), - events.end.format(style)), events.all_day) - - """Convert the event-timings from utc to the specified locale's time - and create a ready-to-display list for the agenda view""" - for events in filtered_events: - if not events.all_day: - agenda_events.append({'date': events.begin, 'time': events.begin.format( - 'HH:mm' if hours == '24' else 'hh:mm a'), 'name':str(events.name), - 'type':'timed_event'}) - else: - if events.duration.days == 1: - agenda_events.append({'date': events.begin,'time': all_day_str, - 'name': events.name,'type':'full_day_event'}) - else: - for day in range(events.duration.days): - agenda_events.append({'date': events.begin.replace(days=+day), - 'time': all_day_str,'name':events.name, 'type':'full_day_event'}) - - """Sort events and dates in chronological order""" - agenda_events = sorted(agenda_events, key = lambda event: event['date']) - - """Crop the agenda_events in case it's too long""" - del agenda_events[max_lines:] - - """Display all events, dates and times on the display""" - if show_events == True: - previous_date = None - for events in range(len(agenda_events)): - if agenda_events[events]['type'] == 'date': - if previous_date == None or previous_date != agenda_events[events][ - 'date']: - write_text(date_col_width, line_height, - agenda_events[events]['date_str'], line_pos[events], font = font) - - previous_date = agenda_events[events]['date'] - - if three_colour_support == True: - draw_col.line((date_col_start, line_pos[events][1], - line_width,line_pos[events][1]), fill = 'black') - else: - draw.line((date_col_start, line_pos[events][1], - line_width,line_pos[events][1]), fill = 'black') - - - elif agenda_events[events]['type'] == 'timed_event': - write_text(time_col_width, line_height, agenda_events[events]['time'], - (time_col_start, line_pos[events][1]), font = font) - - write_text(event_col_width, line_height, ('• '+agenda_events[events][ - 'name']), (event_col_start, line_pos[events][1]), - alignment = 'left', font = font) - - else: - write_text(time_col_width, line_height, agenda_events[events]['time'], - (time_col_start, line_pos[events][1]), font = font) - - write_text(event_col_width, line_height, ('• '+agenda_events[events]['name']), - (event_col_start, line_pos[events][1]), alignment = 'left', font = font) - - """Crop the image to show only the middle section""" - agenda_image = image.crop((0,middle_section_offset,display_width, display_height)) - agenda_image.save(image_path+'inkycal_agenda.png') - - if three_colour_support == True: - agenda_image_col = image_col.crop((0,middle_section_offset,display_width, display_height)) - agenda_image_col.save(image_path+'inkycal_agenda_col.png') - - print('Done') - - except Exception as e: - """If something went wrong, print a Error message on the Terminal""" - print('Failed!') - print('Error in Agenda module!') - print('Reason: ',e) - - clear_image('middle_section') - write_text(middle_section_width, middle_section_height, str(e), - (0, middle_section_offset), font = font) - calendar_image = crop_image(image, 'middle_section') - calendar_image.save(image_path+'inkycal_agenda.png') - pass - - - -def main(): - generate_image() - -main() diff --git a/modules/inkycal_calendar.py b/modules/inkycal_calendar.py deleted file mode 100644 index 0218d55..0000000 --- a/modules/inkycal_calendar.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -Calendar module for Inky-Calendar Project -Copyright by aceisace -""" -from __future__ import print_function -import calendar -from configuration import * - -print_events = False -show_events = True -today_in_your_language = 'today' -tomorrow_in_your_language = 'tomorrow' -at_in_your_language = 'at' -event_icon = 'square' # dot #square -style = "DD MMM" - -font = ImageFont.truetype(NotoSans+'.ttf', calendar_fontsize) -space_between_lines = 0 - -if show_events == True: - from inkycal_icalendar import fetch_events - -"""Add a border to increase readability""" -border_top = int(middle_section_height * 0.02) -border_left = int(middle_section_width * 0.02) - -main_area_height = middle_section_height-border_top*2 -main_area_width = middle_section_width-border_left*2 - -line_height = font.getsize('hg')[1] + space_between_lines -line_width = middle_section_width - (border_left*2) - -"""Calculate height for each sub-section""" -month_name_height = int(main_area_height*0.1) -weekdays_height = int(main_area_height*0.05) -calendar_height = int(main_area_height*0.6) -events_height = int(main_area_height*0.25) - -"""Set rows and coloumns in the calendar section and calculate sizes""" -calendar_rows, calendar_coloumns = 6, 7 -icon_width = main_area_width // calendar_coloumns -icon_height = calendar_height // calendar_rows - -"""Calculate paddings for calendar section""" -x_padding_calendar = int((main_area_width % icon_width) / 2) -y_padding_calendar = int((main_area_height % calendar_rows) / 2) - -"""Add coordinates for number icons inside the calendar section""" -grid_start_y = (middle_section_offset + border_top + month_name_height + - weekdays_height + y_padding_calendar) -grid_start_x = border_left + x_padding_calendar - -grid = [(grid_start_x + icon_width*x, grid_start_y + icon_height*y) - for y in range(calendar_rows) for x in range(calendar_coloumns)] - -weekday_pos = [(grid_start_x + icon_width*_, middle_section_offset + - month_name_height) for _ in range(calendar_coloumns)] - -max_event_lines = (events_height - border_top) // (font.getsize('hg')[1] - + space_between_lines) - -event_lines = [(border_left,(bottom_section_offset - events_height)+ - int(events_height/max_event_lines*_)) for _ in range(max_event_lines)] - -def generate_image(): - if middle_section == "inkycal_calendar" and internet_available() == True: - try: - clear_image('middle_section') - print('Calendar module: Generating image...', end = '') - now = arrow.now(tz = get_tz()) - - """Set up the Calendar template based on personal preferences""" - if week_starts_on == "Monday": - calendar.setfirstweekday(calendar.MONDAY) - weekstart = now.replace(days = - now.weekday()) - else: - calendar.setfirstweekday(calendar.SUNDAY) - weekstart = now.replace(days = - now.isoweekday()) - - """Write the name of the current month at the correct position""" - write_text(main_area_width, month_name_height, - str(now.format('MMMM',locale=language)), (border_left, - middle_section_offset), autofit = True) - - """Set up weeknames in local language and add to main section""" - weekday_names = [weekstart.replace(days=+_).format('ddd',locale=language) - for _ in range(7)] - - for _ in range(len(weekday_pos)): - write_text(icon_width, weekdays_height, weekday_names[_], - weekday_pos[_], autofit = True) - - """Create a calendar template and flatten (remove nestings)""" - flatten = lambda z: [x for y in z for x in y] - calendar_flat = flatten(calendar.monthcalendar(now.year, now.month)) - - """Add the numbers on the correct positions""" - for i in range(len(calendar_flat)): - if calendar_flat[i] not in (0, int(now.day)): - write_text(icon_width, icon_height, str(calendar_flat[i]), grid[i]) - - """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[calendar_flat.index(now.day)] - x_circle,y_circle = int(icon_width/2), int(icon_height/2) - radius = int(icon_width * 0.25) - text_width, text_height = default.getsize(str(now.day)) - x_text = int((icon_width / 2) - (text_width / 2)) - y_text = int((icon_height / 2) - (text_height / 1.7)) - ImageDraw.Draw(icon).ellipse((x_circle-radius, y_circle-radius, - x_circle+radius, y_circle+radius), fill= 'black', outline=None) - ImageDraw.Draw(icon).text((x_text, y_text), str(now.day), fill='white', - font=bold) - if three_colour_support == True: - image_col.paste(icon, current_day_pos, icon) - else: - image.paste(icon, current_day_pos, icon) - - """Create some reference points for the current month""" - days_current_month = calendar.monthrange(now.year, now.month)[1] - month_start = now.floor('month') - month_end = now.ceil('month') - - if show_events == True: - """Filter events which begin before the end of this month""" - upcoming_events = fetch_events() - - calendar_events = [events for events in upcoming_events if - month_start <= events.end <= month_end ] - - """Find days with events in the current month""" - days_with_events = [] - for events in calendar_events: - if events.duration.days <= 1: - days_with_events.append(int(events.begin.format('D'))) - else: - for day in range(events.duration.days): - days_with_events.append( - int(events.begin.replace(days=+i).format('D'))) - days_with_events = set(days_with_events) - - if event_icon == 'dot': - for days in days_with_events: - write_text(icon_width, int(icon_height * 0.2), '•', - (grid[calendar_flat.index(days)][0], - int(grid[calendar_flat.index(days)][1] + icon_height*0.8))) - - if event_icon == 'square': - square_size = int(icon_width * 0.6) - center_x = int((icon_width - square_size) / 2) - center_y = int((icon_height - square_size) / 2) - for days in days_with_events: - draw_square((int(grid[calendar_flat.index(days)][0]+center_x), - int(grid[calendar_flat.index(days)][1] + center_y )), - 8, square_size , square_size, colour='black') - - - """Add a small section showing events of today and tomorrow""" - event_list = [] - after_two_days = now.replace(days=2).floor('day') - - for event in calendar_events: - if event.begin.day == now.day and now < event.end: - if event.all_day: - event_list.append('{}: {}'.format(today_in_your_language, event.name)) - else: - event_list.append('{0} {1} {2} : {3}'.format(today_in_your_language, - at_in_your_language, event.begin.format('HH:mm' if hours == '24' else - 'hh:mm a'), event.name)) - - elif event.begin.day == now.replace(days=1).day: - if event.all_day: - event_list.append('{}: {}'.format(tomorrow_in_your_language, event.name)) - else: - event_list.append('{0} {1} {2} : {3}'.format(tomorrow_in_your_language, - at_in_your_language, event.begin.format('HH:mm' if hours == '24' else - 'hh:mm a'), event.name)) - - elif event.begin > after_two_days: - if event.all_day: - event_list.append('{}: {}'.format(event.begin.format('D MMM'), event.name)) - else: - event_list.append('{0} {1} {2} : {3}'.format(event.begin.format('D MMM'), - at_in_your_language, event.begin.format('HH:mm' if hours == '24' else - 'hh:mm a'), event.name)) - - del event_list[max_event_lines:] - - if event_list: - for lines in event_list: - write_text(main_area_width, int(events_height/max_event_lines), lines, - event_lines[event_list.index(lines)], font=font, alignment='left') - else: - write_text(main_area_width, int(events_height/max_event_lines), - 'No upcoming events.', event_lines[0], font=font, alignment='left') - - """Set print_events_to True to print all events in this month""" - style = 'DD MMM YY HH:mm' - if print_events == True and calendar_events: - line_width = max(len(_.name) for _ in calendar_events) - for events in calendar_events: - print('{0} {1} | {2} | {3} | All day ='.format(events.name, - ' ' * (line_width - len(events.name)), events.begin.format(style), - events.end.format(style)), events.all_day) - - calendar_image = crop_image(image, 'middle_section') - calendar_image.save(image_path+'inkycal_calendar.png') - - if three_colour_support == True: - calendar_image_col = crop_image(image_col, 'middle_section') - calendar_image_col.save(image_path+'inkycal_calendar_col.png') - - print('Done') - - except Exception as e: - """If something went wrong, print a Error message on the Terminal""" - print('Failed!') - print('Error in Calendar module!') - print('Reason: ',e) - clear_image('middle_section') - write_text(middle_section_width, middle_section_height, str(e), - (0, middle_section_offset), font = font) - calendar_image = crop_image(image, 'middle_section') - calendar_image.save(image_path+'inkycal_calendar.png') - pass - -def main(): - generate_image() - -main() diff --git a/modules/inkycal_icalendar.py b/modules/inkycal_icalendar.py deleted file mode 100644 index 421c174..0000000 --- a/modules/inkycal_icalendar.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -iCalendar (parsing) module for Inky-Calendar Project -Copyright by aceisace -""" -from __future__ import print_function -from configuration import * -from settings import ical_urls -import arrow -from ics import Calendar - -use_recurring_events = True -print_events = False -style = 'DD MMM YY HH:mm' - - -if use_recurring_events == True: - from dateutil.rrule import rrulestr, rruleset - import re - -def fetch_events(): - """Set timelines for filtering upcoming events""" - timezone = get_tz() - now = arrow.now(tz=timezone) - beginning_of_month = now.replace(days= - now.day +1) - near_future = now.replace(days= 30) - further_future = now.replace(days=40) - - """Parse the iCalendars from the urls, fixing some known errors with ics""" - calendars = [Calendar(fix_ical(url)) for url in ical_urls] - - """Filter any upcoming events from all iCalendars and add them to a list""" - upcoming_events = [events for ical in calendars for events in ical.events - if beginning_of_month <= events.end <= further_future or - beginning_of_month <= events.begin <= near_future] - - """Try to parse recurring events. This is clearly experimental! """ - if use_recurring_events == True: - for ical in calendars: - for events in ical.events: - event_str = str(events) - if re.search('RRULE:(.+?)\n', event_str): - if events.all_day and events.duration.days > 1: - events.end = events.end.replace(days=-2) - else: - events.end = events.end.to(timezone) - events.begin = events.begin.to(timezone) - try: - rule = re.search('RRULE:(.+?)\n', event_str).group(0)[:-2] - if re.search('UNTIL=(.+?);', rule) and not re.search('UNTIL=(.+?)Z;', rule): - rule = re.sub('UNTIL=(.+?);', 'UNTIL='+re.search('UNTIL=(.+?);', rule).group(0)[6:-1]+'T000000Z;', rule) - dates = rrulestr(rule, dtstart= events.begin.datetime).between(after= now.datetime, before = further_future.datetime) - - if dates: - duration = events.duration - for date in dates: - cc = events.clone() - cc.end = arrow.get(date+duration) - cc.begin = arrow.get(date) - upcoming_events.append(cc) - #print("Added '{}' with new start at {}".format(cc.name, cc.begin.format('DD MMM YY'))) - - except Exception as e: - print('Something went wrong while parsing recurring events') - pass - - """Sort events according to their beginning date""" - def sort_dates(event): - return event.begin - upcoming_events.sort(key=sort_dates) - - """Multiday events are displayed incorrectly; fix that""" - for events in upcoming_events: - if events.all_day and events.duration.days > 1: - events.end = events.end.replace(days=-2) - events.make_all_day() - - if not events.all_day: - events.end = events.end.to(timezone) - events.begin = events.begin.to(timezone) - - """ The list upcoming_events should not be modified. If you need the data from - this one, copy the list or the contents to another one.""" - #print(upcoming_events) # Print all events. Might look a bit messy - - """Print upcoming events in a more appealing way""" - if print_events == True and upcoming_events: - line_width = max(len(i.name) for i in upcoming_events) - for events in upcoming_events: - print('{0} {1} | {2} | {3} | All day ='.format(events.name, - ' '* (line_width - len(events.name)), events.begin.format(style), - events.end.format(style)), events.all_day) - - return upcoming_events diff --git a/modules/inkycal_image.py b/modules/inkycal_image.py deleted file mode 100644 index 6ee4ab8..0000000 --- a/modules/inkycal_image.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -Experimental image module for Inky-Calendar software -Displays an image on the E-Paper. Work in progress! -Copyright by aceisace -""" -from __future__ import print_function -from configuration import * -from os import path -from PIL import ImageOps -import requests -import numpy - -"""----------------------------------------------------------------""" -#path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' -#path ='/home/pi/Inky-Calendar/images/canvas.png' -path = inkycal_image_path -path_body = inkycal_image_path_body -mode = 'auto' # 'horizontal' # 'vertical' # 'auto' -upside_down = True # Flip image by 180 deg (upside-down) -alignment = 'center' # top_center, top_left, center_left, bottom_right etc. -colours = 'bwr' # bwr # bwy # bw -render = True # show image on E-Paper? -"""----------------------------------------------------------------""" - -# First determine dimensions -if mode == 'horizontal': - display_width, display_height == display_height, display_width - -if mode == 'vertical': - pass - -# .. Then substitute possibly parameterized path -# TODO Get (assigned) panel dimensions instead of display dimensions -path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height)) - -"""Try to open the image if it exists and is an image file""" -try: - if 'http' in path: - if path_body is None: - # Plain GET - im = Image.open(requests.get(path, stream=True).raw) - else: - # POST request, passing path_body in the body - im = Image.open(requests.post(path, json=path_body, stream=True).raw) - else: - im = Image.open(path) -except FileNotFoundError: - print('Your file could not be found. Please check the path to your file.') - raise -except OSError: - print('Please check if the path points to an image file.') - raise - -"""Turn image upside-down if specified""" -if upside_down == True: - im.rotate(180, expand = True) - -if mode == 'auto': - if (im.width > im.height) and (display_width < display_height): - print('display vertical, image horizontal -> flipping image') - im = im.rotate(90, expand=True) - if (im.width < im.height) and (display_width > display_height): - print('display horizontal, image vertical -> flipping image') - im = im.rotate(90, expand=True) - -def fit_width(image, width): - """Resize an image to desired width""" - print('resizing width from', image.width, 'to', end = ' ') - wpercent = (display_width/float(image.width)) - hsize = int((float(image.height)*float(wpercent))) - img = image.resize((width, hsize), Image.ANTIALIAS) - print(img.width) - return img - -def fit_height(image, height): - """Resize an image to desired height""" - print('resizing height from', image.height, 'to', end = ' ') - hpercent = (height / float(image.height)) - wsize = int(float(image.width) * float(hpercent)) - img = image.resize((wsize, height), Image.ANTIALIAS) - print(img.height) - return img - -if im.width > display_width: - im = fit_width(im, display_width) -if im.height > display_height: - im = fit_height(im, display_height) - -if alignment == 'center': - x,y = int((display_width-im.width)/2), int((display_height-im.height)/2) -elif alignment == 'center_right': - x, y = display_width-im.width, int((display_height-im.height)/2) -elif alignment == 'center_left': - x, y = 0, int((display_height-im.height)/2) - -elif alignment == 'top_center': - x, y = int((display_width-im.width)/2), 0 -elif alignment == 'top_right': - x, y = display_width-im.width, 0 -elif alignment == 'top_left': - x, y = 0, 0 - -elif alignment == 'bottom_center': - x, y = int((display_width-im.width)/2), display_height-im.height -elif alignment == 'bottom_right': - x, y = display_width-im.width, display_height-im.height -elif alignment == 'bottom_left': - x, y = display_width-im.width, display_height-im.height - -if len(im.getbands()) == 4: - print('removing transparency') - bg = Image.new('RGBA', (im.width, im.height), 'white') - im = Image.alpha_composite(bg, im) - -image.paste(im, (x,y)) -im = image - -if colours == 'bw': - """For black-white images, use monochrome dithering""" - black = im.convert('1', dither=True) -elif colours == 'bwr': - """For black-white-red images, create corresponding palette""" - pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] -elif colours == 'bwy': - """For black-white-yellow images, create corresponding palette""" - pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255] - - -"""Map each pixel of the opened image to the Palette""" -if colours != 'bw': - palette_im = Image.new('P', (3,1)) - palette_im.putpalette(pal * 64) - quantized_im = im.quantize(palette=palette_im) - quantized_im.convert('RGB') - - """Create buffer for coloured pixels""" - buffer1 = numpy.array(quantized_im.convert('RGB')) - r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] - - """Create buffer for black pixels""" - buffer2 = numpy.array(quantized_im.convert('RGB')) - r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] - - if colours == 'bwr': - """Create image for only red pixels""" - buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white - buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black - colour = Image.fromarray(buffer2) - """Create image for only black pixels""" - buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255] - black = Image.fromarray(buffer1) - - if colours == 'bwy': - """Create image for only yellow pixels""" - buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white - buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black - colour = Image.fromarray(buffer2) - """Create image for only black pixels""" - buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] - black = Image.fromarray(buffer1) - -if render == True: - epaper = driver.EPD() - print('Initialising E-Paper...', end = '') - epaper.init() - print('Done') - - print('Sending image data and refreshing display...', end='') - if three_colour_support == True: - epaper.display(epaper.getbuffer(black), epaper.getbuffer(colour)) - else: - epaper.display(epaper.getbuffer(black)) - print('Done') - - print('Sending E-Paper to deep sleep...', end = '') - epaper.sleep() -print('Done') diff --git a/modules/inkycal_rss.py b/modules/inkycal_rss.py deleted file mode 100644 index 0293258..0000000 --- a/modules/inkycal_rss.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -RSS module for Inky-Calendar software. -Copyright by aceisace -""" -from __future__ import print_function -import feedparser -from random import shuffle -from configuration import * - -"""Add a border to increase readability""" -border_top = int(bottom_section_height * 0.05) -border_left = int(bottom_section_width * 0.02) - -"""Choose font optimised for the weather section""" -font = ImageFont.truetype(NotoSans+'.ttf', rss_fontsize) -space_between_lines = 1 -line_height = font.getsize('hg')[1] + space_between_lines -line_width = bottom_section_width - (border_left*2) - -"""Find out how many lines can fit at max in the bottom section""" -max_lines = (bottom_section_height - (border_top*2)) // (font.getsize('hg')[1] - + space_between_lines) - -"""Calculate the height padding so the lines look centralised""" -y_padding = int( (bottom_section_height % line_height) / 2 ) - -"""Create a list containing positions of each line""" -line_positions = [(border_left, bottom_section_offset + - border_top + y_padding + _*line_height ) for _ in range(max_lines)] - -def generate_image(): - if bottom_section == "inkycal_rss" and rss_feeds != [] and internet_available() == True: - try: - clear_image('bottom_section') - print('RSS module: Connectivity check passed. Generating image...', - end = '') - - """Parse the RSS-feed titles & summaries and save them to a list""" - parsed_feeds = [] - for feeds in rss_feeds: - text = feedparser.parse(feeds) - for posts in text.entries: - parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) - - """Shuffle the list, then crop it to the max number of lines""" - shuffle(parsed_feeds) - del parsed_feeds[max_lines:] - - - """Check the lenght of each feed. Wrap the text if it doesn't fit on one line""" - 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 = font, line_width = line_width) - counter += len(filtered_feeds) + len(wrapped) - if counter < max_lines: - filtered_feeds.append(wrapped) - filtered_feeds = flatten(filtered_feeds) - - """Write the correctly formatted text on the display""" - for _ in range(len(filtered_feeds)): - write_text(line_width, line_height, filtered_feeds[_], - line_positions[_], font = font, alignment= 'left') - - del filtered_feeds, parsed_feeds - - rss_image = crop_image(image, 'bottom_section') - rss_image.save(image_path+'inkycal_rss.png') - - if three_colour_support == True: - rss_image_col = crop_image(image_col, 'bottom_section') - rss_image_col.save(image_path+'inkycal_rss_col.png') - - print('Done') - - except Exception as e: - """If something went wrong, print a Error message on the Terminal""" - print('Failed!') - print('Error in RSS module!') - print('Reason: ',e) - clear_image('bottom_section') - write_text(bottom_section_width, bottom_section_height, str(e), - (0, bottom_section_offset), font = font) - rss = crop_image(image, 'bottom_section') - rss.save(image_path+'inkycal_rss.png') - pass - - -def main(): - generate_image() - -main() diff --git a/modules/inkycal_weather.py b/modules/inkycal_weather.py deleted file mode 100644 index 6e361cf..0000000 --- a/modules/inkycal_weather.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -Weather module for Inky-Calendar software. - -The lunar phase calculation is from Sean B. Palmer, inamidst.com. -Thank You Palmer for the awesome code! - -Copyright by aceisace -""" -from __future__ import print_function -import pyowm -from configuration import * -import math, decimal -dec = decimal.Decimal - - -"""Optional parameters""" -round_temperature = True -round_windspeed = True -use_beaufort = True -show_wind_direction = False -use_wind_direction_icon = False -now_str = 'now' - - -"""Set the optional parameters""" -decimal_places_temperature = None if round_temperature == True else 1 -decimal_places_windspeed = None if round_windspeed == True else 1 - -print('Initialising weather...', end=' ') -owm = pyowm.OWM(api_key, language=language) -print('Done') - -"""Icon-code to unicode dictionary for weather-font""" -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' - } - -"""Add a border to increase readability""" -border_top = int(top_section_height * 0.05) -border_left = int(top_section_width * 0.02) - -"""Calculate size for each weather sub-section""" -row_height = (top_section_height-(border_top*2)) // 3 -coloumn_width = (top_section_width-(border_left*2)) // 7 - -"""Calculate paddings""" -x_padding = int( (top_section_width % coloumn_width) / 2 ) -y_padding = int( (top_section_height % row_height) / 2 ) - -"""Allocate sizes for weather icons""" -icon_small = row_height -icon_medium = row_height * 2 - -"""Calculate the x-axis position of each coloumn""" -coloumn1 = x_padding -coloumn2 = coloumn1 + coloumn_width -coloumn3 = coloumn2 + coloumn_width -coloumn4 = coloumn3 + coloumn_width -coloumn5 = coloumn4 + coloumn_width -coloumn6 = coloumn5 + coloumn_width -coloumn7 = coloumn6 + coloumn_width - -"""Calculate the y-axis position of each row""" -row1 = y_padding -row2 = row1 + row_height -row3 = row2 + row_height - -"""Allocate positions for current weather details""" -text_now_pos = (coloumn1, row1) -weather_icon_now_pos = (coloumn1, row2) - -temperature_icon_now_pos = (coloumn2, row1) -temperature_now_pos = (coloumn2+icon_small, row1) -humidity_icon_now_pos = (coloumn2, row2) -humidity_now_pos = (coloumn2+icon_small, row2) -windspeed_icon_now_pos = (coloumn2, row3) -windspeed_now_pos = (coloumn2+icon_small, row3) - -moon_phase_now_pos = (coloumn3, row1) -sunrise_icon_now_pos = (coloumn3, row2) -sunrise_time_now_pos = (coloumn3+icon_small, row2) -sunset_icon_now_pos = (coloumn3, row3) -sunset_time_now_pos = (coloumn3+ icon_small, row3) - -"""Allocate positions for weather forecast after 3 hours""" -text_fc1_pos = (coloumn4, row1) -icon_fc1_pos = (coloumn4, row2) -temperature_fc1_pos = (coloumn4, row3) - -"""Allocate positions for weather forecast after 6 hours""" -text_fc2_pos = (coloumn5, row1) -icon_fc2_pos = (coloumn5, row2) -temperature_fc2_pos = (coloumn5, row3) - -"""Allocate positions for weather forecast after 9 hours""" -text_fc3_pos = (coloumn6, row1) -icon_fc3_pos = (coloumn6, row2) -temperature_fc3_pos = (coloumn6, row3) - -"""Allocate positions for weather forecast after 12 hours""" -text_fc4_pos = (coloumn7, row1) -icon_fc4_pos = (coloumn7, row2) -temperature_fc4_pos = (coloumn7, row3) - -"""Windspeed (m/s) to beaufort (index of list) conversion""" -windspeed_to_beaufort = [0.02, 1.5, 3.3, 5.4, 7.9, 10.7, 13.8, 17.1, 20.7, - 24.4, 28.4, 32.6, 100] - -def to_units(kelvin): - """Function to convert tempertures from kelvin to celcius or fahrenheit""" - degrees_celsius = round(kelvin - 273.15, ndigits = decimal_places_temperature) - fahrenheit = round((kelvin - 273.15) * 9/5 + 32, - ndigits = decimal_places_temperature) - if units == 'metric': - conversion = str(degrees_celsius) + '°C' - - if units == 'imperial': - conversion = str(fahrenheit) + 'F' - - return conversion - -def red_temp(negative_temperature): - if three_colour_support == True and negative_temperature[0] == '-' and units == 'metric': - colour = 'red' - else: - colour = 'black' - return colour - -"""Function to convert time objects to specified format 12/24 hours""" -"""Simple means just the hour and if 12 hours, am/pm as well""" -def to_hours(datetime_object, simple = False): - if hours == '24': - if simple == True: - converted_time = datetime_object.format('H') + '.00' - else: - converted_time = datetime_object.format('HH:mm') - else: - if simple == True: - converted_time = datetime_object.format('H a') - else: - converted_time = datetime_object.format('hh:mm') - return str(converted_time) - -"""Choose font optimised for the weather section""" -fontsize = 8 -font = ImageFont.truetype(NotoSans+'Medium.ttf', fontsize) -fill_height = 0.8 - -while font.getsize('hg')[1] <= (row_height * fill_height): - fontsize += 1 - font = ImageFont.truetype(NotoSans+'.ttf', fontsize) - -def generate_image(): - """Connect to Openweathermap API and fetch weather data""" - if top_section == "inkycal_weather" and api_key != "" and owm.is_API_online() is True: - try: - clear_image('top_section') - print('Weather module: Connectivity check passed, Generating image...', - end = '') - current_weather_setup = owm.weather_at_place(location) - weather = current_weather_setup.get_weather() - - """Set-up and get weather forecast data""" - forecast = owm.three_hours_forecast(location) - - """Round the hour to the nearest multiple of 3""" - now = arrow.utcnow() - if (now.hour % 3) != 0: - hour_gap = 3 - (now.hour % 3) - else: - hour_gap = 3 - - """Prepare timings for forecasts""" - fc1 = now.replace(hours = + hour_gap).floor('hour') - fc2 = now.replace(hours = + hour_gap + 3).floor('hour') - fc3 = now.replace(hours = + hour_gap + 6).floor('hour') - fc4 = now.replace(hours = + hour_gap + 9).floor('hour') - - """Prepare forecast objects for the specified timings""" - forecast_fc1 = forecast.get_weather_at(fc1.datetime) - forecast_fc2 = forecast.get_weather_at(fc2.datetime) - forecast_fc3 = forecast.get_weather_at(fc3.datetime) - forecast_fc4 = forecast.get_weather_at(fc4.datetime) - - """Get the current temperature and forcasts temperatures""" - temperature_now = to_units(weather.get_temperature()['temp']) - temperature_fc1 = to_units(forecast_fc1.get_temperature()['temp']) - temperature_fc2 = to_units(forecast_fc2.get_temperature()['temp']) - temperature_fc3 = to_units(forecast_fc3.get_temperature()['temp']) - temperature_fc4 = to_units(forecast_fc4.get_temperature()['temp']) - - """Get current and forecast weather icon names""" - weather_icon_now = weather.get_weather_icon_name() - weather_icon_fc1 = forecast_fc1.get_weather_icon_name() - weather_icon_fc2 = forecast_fc2.get_weather_icon_name() - weather_icon_fc3 = forecast_fc3.get_weather_icon_name() - weather_icon_fc4 = forecast_fc4.get_weather_icon_name() - - """Parse current weather details""" - sunrise_time_now = arrow.get(weather.get_sunrise_time()).to(get_tz()) - sunset_time_now = arrow.get(weather.get_sunset_time()).to(get_tz()) - humidity_now = str(weather.get_humidity()) - cloudstatus_now = str(weather.get_clouds()) - weather_description_now = str(weather.get_detailed_status()) - windspeed_now = weather.get_wind(unit='meters_sec')['speed'] - wind_degrees = forecast_fc1.get_wind()['deg'] - wind_direction = ["N","NE","E","SE","S","SW","W","NW"][round( - wind_degrees/45) % 8] - - if use_beaufort == True: - wind = str([windspeed_to_beaufort.index(_) for _ in - windspeed_to_beaufort if windspeed_now < _][0]) - else: - meters_sec = round(windspeed_now, ndigits = windspeed_decimal_places) - miles_per_hour = round(windspeed_now * 2,23694, - ndigits = windspeed_decimal_places) - if units == 'metric': - wind = str(meters_sec) + 'm/s' - if units == 'imperial': - wind = str(miles_per_hour) + 'mph' - if show_wind_direction == True: - wind += '({0})'.format(wind_direction) - - """Calculate the moon phase""" - def get_moon_phase(): - 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] - - moonphase = get_moon_phase() - - """Add weather details in column 1""" - write_text(coloumn_width, row_height, now_str, text_now_pos, font = font) - write_text(icon_medium, icon_medium, weathericons[weather_icon_now], - weather_icon_now_pos, font = w_font, fill_width = 0.9) - - """Add weather details in column 2""" - write_text(icon_small, icon_small, '\uf053', temperature_icon_now_pos, - font = w_font, fill_height = 0.9) - write_text(icon_small, icon_small, '\uf07a', humidity_icon_now_pos, - font = w_font, fill_height = 0.9) - - if use_wind_direction_icon == False: - write_text(icon_small, icon_small, '\uf050', windspeed_icon_now_pos, - font = w_font, fill_height = 0.9) - else: - write_text(icon_small, icon_small, '\uf0b1', windspeed_icon_now_pos, - font = w_font, fill_height = 0.9, rotation = -wind_degrees) - - write_text(coloumn_width-icon_small, row_height, temperature_now, - temperature_now_pos, font = font, colour= red_temp(temperature_now)) - - - write_text(coloumn_width-icon_small, row_height, humidity_now+'%', - humidity_now_pos, font = font) - write_text(coloumn_width-icon_small, row_height, wind, - windspeed_now_pos, font = font, autofit = True) - - """Add weather details in column 3""" - write_text(coloumn_width, row_height, moonphase , moon_phase_now_pos, - font = w_font, fill_height = 0.9) - write_text(icon_small, icon_small, '\uf051', sunrise_icon_now_pos, - font = w_font, fill_height = 0.9) - write_text(icon_small, icon_small, '\uf052', sunset_icon_now_pos, - font = w_font, fill_height = 0.9) - - write_text(coloumn_width-icon_small, row_height, - to_hours(sunrise_time_now), sunrise_time_now_pos, font = font, - fill_width = 0.9) - write_text(coloumn_width-icon_small, row_height, - to_hours(sunset_time_now), sunset_time_now_pos, font = font, - fill_width = 0.9) - - """Add weather details in column 4 (forecast 1)""" - write_text(coloumn_width, row_height, to_hours(fc1, simple=True), - text_fc1_pos, font = font) - write_text(coloumn_width, row_height, weathericons[weather_icon_fc1], - icon_fc1_pos, font = w_font, fill_height = 1.0) - write_text(coloumn_width, row_height, temperature_fc1, - temperature_fc1_pos, font = font, colour = red_temp( - temperature_fc1)) - - """Add weather details in column 5 (forecast 2)""" - write_text(coloumn_width, row_height, to_hours(fc2, simple=True), - text_fc2_pos, font = font) - write_text(coloumn_width, row_height, weathericons[weather_icon_fc2], - icon_fc2_pos, font = w_font, fill_height = 1.0) - write_text(coloumn_width, row_height, temperature_fc2, - temperature_fc2_pos, font = font, colour = red_temp( - temperature_fc2)) - - """Add weather details in column 6 (forecast 3)""" - write_text(coloumn_width, row_height, to_hours(fc3, simple=True), - text_fc3_pos, font = font) - write_text(coloumn_width, row_height, weathericons[weather_icon_fc3], - icon_fc3_pos, font = w_font, fill_height = 1.0) - write_text(coloumn_width, row_height, temperature_fc3, - temperature_fc3_pos, font = font, colour = red_temp( - temperature_fc3)) - - """Add weather details in coloumn 7 (forecast 4)""" - write_text(coloumn_width, row_height, to_hours(fc4, simple=True), - text_fc4_pos, font = font) - write_text(coloumn_width, row_height, weathericons[weather_icon_fc4], - icon_fc4_pos, font = w_font, fill_height = 1.0) - write_text(coloumn_width, row_height, temperature_fc4, - temperature_fc4_pos, font = font, colour = red_temp( - temperature_fc4)) - - """Add vertical lines between forecast sections""" - draw = ImageDraw.Draw(image) - line_start_y = int(top_section_height*0.1) - line_end_y = int(top_section_height*0.9) - - draw.line((coloumn4, line_start_y, coloumn4, line_end_y), fill='black') - draw.line((coloumn5, line_start_y, coloumn5, line_end_y), fill='black') - draw.line((coloumn6, line_start_y, coloumn6, line_end_y), fill='black') - draw.line((coloumn7, line_start_y, coloumn7, line_end_y), fill='black') - - if three_colour_support == True: - draw_col.line((0, top_section_height-border_top, top_section_width- - border_left, top_section_height-border_top), fill='black', width=3) - else: - draw.line((0, top_section_height-border_top, top_section_width- - border_left, top_section_height-border_top), fill='black', width=3) - - weather_image = crop_image(image, 'top_section') - weather_image.save(image_path+'inkycal_weather.png') - - if three_colour_support == True: - weather_image_col = crop_image(image_col, 'top_section') - weather_image_col.save(image_path+'inkycal_weather_col.png') - - print('Done') - - except Exception as e: - """If something went wrong, print a Error message on the Terminal""" - print('Failed!') - print('Error in weather module!') - print('Reason: ',e) - clear_image('top_section') - write_text(top_section_width, top_section_height, str(e), - (0, 0), font = font) - weather_image = crop_image(image, 'top_section') - weather_image.save(image_path+'inkycal_weather.png') - pass - -def main(): - generate_image() - -main() diff --git a/release.txt b/release.txt deleted file mode 100644 index 3b34d22..0000000 --- a/release.txt +++ /dev/null @@ -1 +0,0 @@ -v1.7.2 diff --git a/requirements.txt b/requirements.txt index 5677628..2ab84cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ pyowm==2.10.0 -Pillow==6.2.0 -ics==0.4 +Pillow==7.1.1 +ics==0.7 feedparser==5.2.1 -pytz==2019.3 +python-dateutil==2.6.1 +numpy==1.18.2 diff --git a/settings/settings-UI.html b/settings-UI.html similarity index 100% rename from settings/settings-UI.html rename to settings-UI.html diff --git a/settings/configuration.py b/settings/configuration.py deleted file mode 100644 index 361afe2..0000000 --- a/settings/configuration.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -Advanced configuration options for Inky-Calendar software. -Contains some useful functions for correctly rendering text, -calibrating (E-Paper display), checking internet connectivity - -Copyright by aceisace -""" -from PIL import Image, ImageDraw, ImageFont, ImageColor -import numpy -import arrow -from urllib.request import urlopen -from settings import * -from pytz import timezone -import os -from glob import glob -import importlib -import subprocess as subp - -"""Set the image background colour and text colour""" -background_colour = 'white' -text_colour = 'black' - -"""Set some display parameters""" -driver = importlib.import_module('drivers.'+model) -display_height, display_width = driver.EPD_WIDTH, driver.EPD_HEIGHT - -"""Check if the display supports 3 colours""" -if 'colour' in model: - three_colour_support = True -else: - three_colour_support = False - -"""Create 3 sections of the display, based on percentage""" -top_section_width = middle_section_width = bottom_section_width = display_width - -if top_section and bottom_section: - top_section_height = int(display_height*0.11) - bottom_section_height = int(display_height*0.24) - -elif top_section and not bottom_section: - top_section_height = int(display_height*0.11) - bottom_section_height = 0 - -elif bottom_section and not top_section: - top_section_height = 0 - bottom_section_height = int(display_height*0.24) - -elif not top_section and not bottom_section: - top_section_height = bottom_section_height = 0 - -middle_section_height = int(display_height - top_section_height - - bottom_section_height) - -"""Find out the y-axis position of each section""" -top_section_offset = 0 -middle_section_offset = top_section_height -bottom_section_offset = display_height - bottom_section_height - -"""Get the relative path of the Inky-Calendar folder""" -path = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/") -if path != "" and path[-1] != "/": - path += "/" -while not path.endswith('/Inky-Calendar/'): - path = ''.join(list(path)[:-1]) - -"""Select path for saving temporary image files""" -image_path = path + 'images/' - -"""Fonts handling""" -fontpath = path+'fonts/' -NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-' -NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed' -weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf' - -"""Fontsizes""" -default_fontsize = 18 -agenda_fontsize = 14 -calendar_fontsize = 14 -rss_fontsize = 14 -weather_fontsize = 12 - -"""Automatically select correct fonts to support set language""" -if language in ['ja','zh','zh_tw','ko']: - default = ImageFont.truetype(NotoSansCJK+'Regular.otf', default_fontsize) - semi = ImageFont.truetype(NotoSansCJK+'Medium.otf', default_fontsize) - bold = ImageFont.truetype(NotoSansCJK+'Bold.otf', default_fontsize) -else: - default = ImageFont.truetype(NotoSans+'.ttf', default_fontsize) - semi = ImageFont.truetype(NotoSans+'Medium.ttf', default_fontsize) - bold = ImageFont.truetype(NotoSans+'SemiBold.ttf', default_fontsize) - -w_font = ImageFont.truetype(weatherfont, weather_fontsize) - -"""Create a blank image for black pixels and a colour image for coloured pixels""" -image = Image.new('RGB', (display_width, display_height), background_colour) -image_col = Image.new('RGB', (display_width, display_height), 'white') - -draw = ImageDraw.Draw(image) -draw_col = ImageDraw.Draw(image_col) - - -"""Custom function to add text on an image""" -def write_text(space_width, space_height, text, tuple, - font=default, alignment='middle', autofit = False, fill_width = 1.0, - fill_height = 0.8, colour = text_colour, rotation = None): - """tuple refers to (x,y) position on display""" - 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(space_width * fill_width) and text_height < int(space_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] - - while (text_width, text_height) > (space_width, space_height): - text=text[0:-1] - text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] - if alignment is "" or "middle" or None: - x = int((space_width / 2) - (text_width / 2)) - if alignment is 'left': - x = 0 - if font != w_font: - y = int((space_height / 2) - (text_height / 1.7)) - else: - y = y = int((space_height / 2) - (text_height / 2)) - - space = Image.new('RGBA', (space_width, space_height)) - ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font) - if rotation != None: - space.rotate(rotation, expand = True) - - if colour == 'black' or 'white': - image.paste(space, tuple, space) - else: - image_col.paste(space, tuple, space) - -def clear_image(section, colour = background_colour): - """Clear the image""" - width, height = eval(section+'_width'), eval(section+'_height') - position = (0, eval(section+'_offset')) - box = Image.new('RGB', (width, height), colour) - image.paste(box, position) - - if three_colour_support == True: - image_col.paste(box, position) - - -def crop_image(input_image, section): - """Crop an input image to the desired section""" - x1, y1 = 0, eval(section+'_offset') - x2, y2 = eval(section+'_width'), y1 + eval(section+'_height') - image = input_image.crop((x1,y1,x2,y2)) - return image - -def text_wrap(text, font=default, line_width = display_width): - """Split long text into smaller lists""" - counter, padding = 0, 40 - lines = [] - if font.getsize(text)[0] < line_width: - lines.append(text) - else: - for i in range(1, len(text.split())+1): - line = ' '.join(text.split()[counter:i]) - if not font.getsize(line)[0] < line_width - padding: - lines.append(line) - line, counter = '', i - if i == len(text.split()) and line != '': - lines.append(line) - return lines - - -def draw_square(tuple, radius, width, height, colour=text_colour, line_width=1): - """Draws a square with round corners at position (x,y) from tuple""" - x, y, diameter = tuple[0], tuple[1], radius*2 - line_length = width - diameter - - p1, p2 = (x+radius, y), (x+radius+line_length, y) - p3, p4 = (x+width, y+radius), (x+width, y+radius+line_length) - p5, p6 = (p2[0], y+height), (p1[0], y+height) - p7, p8 = (x, p4[1]), (x,p3[1]) - 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) - - if three_colour_support == True: - draw_col.line( (p1, p2) , fill=colour, width = line_width) - draw_col.line( (p3, p4) , fill=colour, width = line_width) - draw_col.line( (p5, p6) , fill=colour, width = line_width) - draw_col.line( (p7, p8) , fill=colour, width = line_width) - draw_col.arc( (c1, c2) , 180, 270, fill=colour, width=line_width) - draw_col.arc( (c3, c4) , 270, 360, fill=colour, width=line_width) - draw_col.arc( (c5, c6) , 0, 90, fill=colour, width=line_width) - draw_col.arc( (c7, c8) , 90, 180, fill=colour, width=line_width) - else: - draw.line( (p1, p2) , fill=colour, width = line_width) - draw.line( (p3, p4) , fill=colour, width = line_width) - draw.line( (p5, p6) , fill=colour, width = line_width) - draw.line( (p7, p8) , fill=colour, width = line_width) - draw.arc( (c1, c2) , 180, 270, fill=colour, width=line_width) - draw.arc( (c3, c4) , 270, 360, fill=colour, width=line_width) - draw.arc( (c5, c6) , 0, 90, fill=colour, width=line_width) - draw.arc( (c7, c8) , 90, 180, fill=colour, width=line_width) - - -def internet_available(): - """check if the internet is available""" - try: - urlopen('https://google.com',timeout=5) - return True - except URLError as err: - return False - - -def get_tz(): - """Get the system timezone""" - with open('/etc/timezone','r') as file: - lines = file.readlines() - system_tz = lines[0].rstrip() - local_tz = timezone(system_tz) - return local_tz - -def fix_ical(ical_url): - """Use iCalendars in compatability mode (without alarms)""" - ical = str(urlopen(ical_url).read().decode()) - beginAlarmIndex = 0 - while beginAlarmIndex >= 0: - beginAlarmIndex = ical.find('BEGIN:VALARM') - if beginAlarmIndex >= 0: - endAlarmIndex = ical.find('END:VALARM') - ical = ical[:beginAlarmIndex] + ical[endAlarmIndex+12:] - return ical - -def image_cleanup(): - """Delete all files in the image folder""" - print('Cleanup of previous images...', end = '') - for temp_files in glob(image_path+'*'): - os.remove(temp_files) - print('Done') - -def optimise_colours(image, threshold=220): - buffer = numpy.array(image.convert('RGB')) - red, green = buffer[:, :, 0], buffer[:, :, 1] - buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] #grey->black - image = Image.fromarray(buffer) - return image - -def calibrate_display(no_of_cycles): - """How many times should each colour be calibrated? Default is 3""" - epaper = driver.EPD() - epaper.init() - - white = Image.new('1', (display_width, display_height), 'white') - black = Image.new('1', (display_width, display_height), 'black') - - print('----------Started calibration of E-Paper display----------') - if 'colour' in model: - for _ in range(no_of_cycles): - print('Calibrating...', end= ' ') - print('black...', end= ' ') - epaper.display(epaper.getbuffer(black), epaper.getbuffer(white)) - print('colour...', end = ' ') - epaper.display(epaper.getbuffer(white), epaper.getbuffer(black)) - print('white...') - epaper.display(epaper.getbuffer(white), epaper.getbuffer(white)) - print('Cycle {0} of {1} complete'.format(_+1, no_of_cycles)) - else: - for _ in range(no_of_cycles): - print('Calibrating...', end= ' ') - print('black...', end = ' ') - epaper.display(epaper.getbuffer(black)) - print('white...') - epaper.display(epaper.getbuffer(white)), - print('Cycle {0} of {1} complete'.format(_+1, no_of_cycles)) - - print('-----------Calibration complete----------') - epaper.sleep() - -def check_for_updates(): - with open(path+'release.txt','r') as file: - lines = file.readlines() - installed_release = lines[0].rstrip() - - temp = subp.check_output(['curl','-s','https://github.com/aceisace/Inky-Calendar/releases/latest']) - latest_release_url = str(temp).split('"')[1] - latest_release = latest_release_url.split('/tag/')[1] - - def get_id(version): - if not version.startswith('v'): - print('incorrect release format!') - v = ''.join(version.split('v')[1].split('.')) - if len(v) == 2: - v += '0' - return int(v) - - if get_id(installed_release) < get_id(latest_release): - print('New update available!. Please update to the latest version') - print('current release:', installed_release, 'new version:', latest_release) - else: - print('You are using the latest version of the Inky-Calendar software:', end = ' ') - print(installed_release) diff --git a/settings/init.py b/settings/init.py deleted file mode 100644 index 9b2c532..0000000 --- a/settings/init.py +++ /dev/null @@ -1 +0,0 @@ -#nothing in here. What did you expect? \ No newline at end of file diff --git a/settings/settings.jsonc b/settings/settings.jsonc deleted file mode 100644 index 7277439..0000000 --- a/settings/settings.jsonc +++ /dev/null @@ -1,91 +0,0 @@ -{ - "language" : "en", // "en", "de", "fr", "jp" etc. - "units" : "metric", // "metric", "imperial" - "hours" : 24, // 24, 12 - "model" : "epd_7_in_5_v2_colour", // For supported E-paper models, see below - "update_interval" : 60, // 10, 15, 20, 30, 60 - "calibration_hours" : [0,12,18], // Do not change unlesss you know what you are doing - - //For now three panels can be defined for three unique locations: 'top', 'middle' and 'bottom' - "panels" : [ - { - "location" : "top", - "type" : "inkycal_weather", - "config" : { - "api_key" : "", //Your openweathermap API-KEY -> "api-key" - "location" : "Stuttgart, DE" //"City name, Country code" - } - }, - { - "location" : "middle", - "type" : "inkycal_calendar", // "inkycal_calendar" and "inkycal_agenda" have the same parameters, but are displayed differently - "config" : { - "week_starts_on" : "Monday", //"Sunday", "Monday"... - "ical_urls" : [ - "https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics", - "https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics" - ] - } - }, - { - "location" : "bottom", - "type" : "inkycal_rss", - "config" : { - "rss_feeds" : [ - "http://feeds.bbci.co.uk/news/world/rss.xml#", - "https://github.com/aceisace/Inky-Calendar/releases.atom" - ] - } - }, - { - "location" : "middle", - "type" : "inkycal_image", - "config" : { - /* - The url or file path to obtain the image from. - The following parameters within accolades ({}) will be substituted: - - model - - width - - height - - Samples - The inkycal logo: - inkycal_image_path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' - - A dynamic image with a demo-calendar - inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/test/{model}/image?width={width}&height={height}' - - Dynamic image with configurable calendars (see https://inkycal.robertsirre.nl/ and parameter inkycal_image_path_body) - inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}' - - inkycal_image_path ='/home/pi/Inky-Calendar/images/canvas.png' - */ - "image_path" : "https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png", - - /* - Optional: inkycal_image_path_body - Allows obtaining complexer configure images. - When `inkycal_image_path` starts with `http` and `inkycal_image_path_body` is specified, the image is obtained using POST instead of GET. - NOTE: structure of the body depends on the web-based image service - */ - - // inkycal_image_path_body = [ - // 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics', - // 'https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics' - // ] - } - } - ] -} - -/* -Supported E-Paper models""" -epd_7_in_5_v2_colour # 7.5" high-res black-white-red/yellow -epd_7_in_5_v2 # 7.5" high-res black-white -epd_7_in_5_colour # 7.5" black-white-red/yellow -epd_7_in_5 # 7.5" black-white -epd_5_in_83_colour # 5.83" black-white-red/yellow -epd_5_in_83 # 5.83" black-white -epd_4_in_2_colour # 4.2" black-white-red/yellow -epd_4_in_2 # 4.2" black-white -*/ \ No newline at end of file diff --git a/settings/settings.py b/settings/settings.py deleted file mode 100644 index 5b46cf3..0000000 --- a/settings/settings.py +++ /dev/null @@ -1,66 +0,0 @@ -ical_urls = ["https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics"] #iCal URLs -rss_feeds = ["http://feeds.bbci.co.uk/news/world/rss.xml#"] # Use any RSS feed -update_interval = "60" # "10" # "15" # "20" # "30" # "60" -api_key = "" # Your openweathermap API-KEY -> "api-key" -location = "Stuttgart, DE" # "City name, Country code" -week_starts_on = "Monday" # 'Sunday' # Monday -calibration_hours = [0,12,18] # Do not change unlesss you know what you are doing -model = "epd_7_in_5_v2_colour" # Choose the E-Paper model (see below) -language = "en" # "en" # "de" # "fr" # "jp" etc. -units = "metric" # "metric" # "imperial" -hours = "24" # "24" # "12" -top_section = "inkycal_weather" # "inkycal_weather" -middle_section = "inkycal_calendar" # "inkycal_agenda" #"inkycal_calendar" -bottom_section = "inkycal_rss" # "inkycal_rss" - -"""Adding multiple iCalendar URLs or RSS feed URLs""" -# Single URL: -# ical_urls/rss_feeds = ["url1"] - -# Multiple URLs: -# ical_urls/rss_feeds = ["url1", "url2", "url3"] - -# URLs should have this sign (") on both side -> "url1" -# If more than one URL is used, separate each one with a comma -> "url1", "url2" - -######################## -# inkycal_image config: -# -# inkycal_image_path -# The url or file path to obtain the image from. -# The following parameters within accolades ({}) will be substituted: -# - model -# - width -# - height -# -# Samples : -# The inkycal logo: -# inkycal_image_path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' -# -# A dynamic image with a demo-calendar -# inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/test/{model}/image?width={width}&height={height}' -# -# Dynamic image with configurable calendars (see https://inkycal.robertsirre.nl/ and parameter inkycal_image_path_body) -# inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}' - -inkycal_image_path ='/home/pi/Inky-Calendar/images/canvas.png' - -# Optional: inkycal_image_path_body -# Allows obtaining complexer configure images. -# When inkycal_image_path starts with `http` and inkycal_image_path_body is specified, the image is obtained using POST instead of GET. -# NOTE: structure of the body depends on the web-based image service -# inkycal_image_path_body = [ -# 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics', -# 'https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics' -# ] -######################## - -"""Supported E-Paper models""" -# epd_7_in_5_v2_colour # 7.5" high-res black-white-red/yellow -# epd_7_in_5_v2 # 7.5" high-res black-white -# epd_7_in_5_colour # 7.5" black-white-red/yellow -# epd_7_in_5 # 7.5" black-white -# epd_5_in_83_colour # 5.83" black-white-red/yellow -# epd_5_in_83 # 5.83" black-white -# epd_4_in_2_colour # 4.2" black-white-red/yellow -# epd_4_in_2 # 4.2" black-white