Folder restructuring
Reorganised folders and files
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.gitignore.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | *.pyc | ||||||
|  | __pycache__/ | ||||||
|  | _build | ||||||
|  | _static | ||||||
|  | _templates | ||||||
| @@ -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' |  | ||||||
							
								
								
									
										45
									
								
								inkycal/Inkycal.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								inkycal/Inkycal.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  |      | ||||||
							
								
								
									
										0
									
								
								inkycal/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								inkycal/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										2
									
								
								inkycal/configuration/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								inkycal/configuration/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | from .settings_parser import inkycal_settings | ||||||
|  | print('loaded settings') | ||||||
							
								
								
									
										42
									
								
								inkycal/configuration/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								inkycal/configuration/settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -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#" | ||||||
|  | 				] | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	] | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								inkycal/configuration/settings_parser.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								inkycal/configuration/settings_parser.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||||
							
								
								
									
										2
									
								
								inkycal/display/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								inkycal/display/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | from .layout import inkycal_layout | ||||||
|  | print('imported layout class') | ||||||
							
								
								
									
										82
									
								
								inkycal/display/layout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								inkycal/display/layout.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
							
								
								
									
										0
									
								
								inkycal/display/operations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								inkycal/display/operations.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								inkycal/modules/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								inkycal/modules/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										140
									
								
								inkycal/modules/inkycal_rss.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								inkycal/modules/inkycal_rss.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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])) | ||||||
							
								
								
									
										1
									
								
								inkycal/render/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								inkycal/render/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | from .fonts import inkycal_fonts | ||||||
							
								
								
									
										165
									
								
								inkycal/render/functions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								inkycal/render/functions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
| @@ -1 +0,0 @@ | |||||||
| This is just a dummy file. |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| #nothing in here. What did you expect? |  | ||||||
| @@ -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) |  | ||||||
| @@ -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() |  | ||||||
| @@ -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() |  | ||||||
| @@ -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 |  | ||||||
| @@ -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') |  | ||||||
| @@ -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() |  | ||||||
| @@ -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() |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| v1.7.2 |  | ||||||
| @@ -1,5 +1,6 @@ | |||||||
| pyowm==2.10.0 | pyowm==2.10.0 | ||||||
| Pillow==6.2.0 | Pillow==7.1.1 | ||||||
| ics==0.4 | ics==0.7 | ||||||
| feedparser==5.2.1 | feedparser==5.2.1 | ||||||
| pytz==2019.3 | python-dateutil==2.6.1 | ||||||
|  | numpy==1.18.2 | ||||||
|   | |||||||
| @@ -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) |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| #nothing in here. What did you expect? |  | ||||||
| @@ -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 |  | ||||||
| */ |  | ||||||
| @@ -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 |  | ||||||
		Reference in New Issue
	
	Block a user