Inititial commit for release v2.0.0
A lot of work-in-progress and far from complete. Lots of improvements related to user-friendliness, fully new web-UI. Better infrastructure.... more coming soon
This commit is contained in:
		
							
								
								
									
										173
									
								
								Installer.sh
									
									
									
									
									
								
							
							
						
						
									
										173
									
								
								Installer.sh
									
									
									
									
									
								
							| @@ -1,173 +0,0 @@ | ||||
| #!/bin/bash | ||||
| # E-Paper-Calendar software installer for Raspberry Pi running Debian 10 (a.k.a. Buster) with Desktop | ||||
| # Version: 1.7.2 (Mid Feb 2020) | ||||
|  | ||||
| echo -e "\e[1mPlease select an option from below:" | ||||
| echo -e "\e[97mEnter \e[91m[1]\e[97m to update Inky-Calendar software"        #Option 1 : UPDATE | ||||
| echo -e "\e[97mEnter \e[91m[2]\e[97m to install Inky-Calendar software"       #Option 2 : INSTALL | ||||
| echo -e "\e[97mEnter \e[91m[3]\e[97m to uninstall Inky-Calendar software"     #Option 3 : UNINSTALL | ||||
| echo -e "\e[97mConfirm your selection with [ENTER]" | ||||
| read -r -p 'Waiting for input...  ' option | ||||
|  | ||||
| # Invalid number selected, abort | ||||
| if [ "$option" != 1 ] && [ "$option" != 2 ] && [ "$option" != 3 ]; then echo -e "invalid number, aborting now" exit | ||||
| fi | ||||
|  | ||||
| # No option selected, abort | ||||
| if [ -z "$option" ]; then echo -e "You didn't enter anything, aborting now." exit | ||||
| fi | ||||
|  | ||||
|  # What to do when uninstalling software | ||||
| if [ "$option" = 3 ]; then | ||||
|  | ||||
|     # Remove requirements of software | ||||
|     echo -e "\e[1;36m"Removing requirements for Inky-Calendar software"\e[0m" | ||||
|     cd /home/"$USER"/Inky-Calendar && pip3 uninstall -r requirements.txt && sudo apt-get clean && sudo apt-get autoremove -y | ||||
|  | ||||
|     # Remove configuration file for supervisor if it exists | ||||
|     if [ -e /etc/supervisor/conf.d/inkycal.conf ]; then sudo rm /etc/supervisor/conf.d/inkycal.conf | ||||
|     fi | ||||
|  | ||||
|     # Print message that libraries have been uninstalled now | ||||
|     echo -e "\e[1;36m"The libraries have been removed successfully"\e[0m" | ||||
|     sleep 2 | ||||
|  | ||||
|     # Remove the Inky-Calendar directory if it exists | ||||
|     echo -e "Removing the Inky-Calendar folder if it exists" | ||||
|     if [ -d "/home/$USER/Inky-Calendar" ]; then | ||||
|         sudo rm -r /home/"$USER"/Inky-Calendar/ | ||||
| 	echo -e "\e[1;36m"Found Inky-Calendar folder and deleted it"\e[0m" | ||||
|     fi | ||||
|     echo -e "\e[1;36m"All done!"\e[0m" | ||||
| fi | ||||
|  | ||||
| if [ "$option" = 1 ]; then # UPDATE software | ||||
|     echo -e "\e[1;36m"Checking if the Inky-Calendar folder exists..."\e[0m" | ||||
|     if [ -d "/home/$USER/Inky-Calendar" ]; then | ||||
|         echo -e "Found Inky-Calendar directory in /home/$USER" | ||||
| 	sleep 2 | ||||
|         echo -e "To prevent overwriting the Inky-Calendar folder, the installer will not continue." | ||||
| 	echo -e "Please rename the Inky-Calendar folder and then re-run the installer" | ||||
| 	exit | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| if [ "$option" = 1 ] || [ "$option" = 2 ]; then # This happens when installing or updating | ||||
|     # Ask to update system | ||||
|     echo -e "\e[1;36m"Would you like to update and upgrade the operating system first?"\e[0m" | ||||
|     sleep 1 | ||||
|     echo -e "\e[97mIt is not scrictly required, but highly recommended." | ||||
|     sleep 1 | ||||
|     echo -e "\e[97mPlease note that updating may take quite some time, in rare cases up to 1 hour." | ||||
|     sleep 1 | ||||
|     echo -e "\e[97mPlease type [y] for yes or [n] for no and confirm your selection with [ENTER]" | ||||
|     read -r -p 'Waiting for input...  ' update | ||||
|      | ||||
|     if [ "$update" != Y ] && [ "$update" != y ] && [ "$update" != N ] && [ "$update" != n ]; then echo -e "invalid input, aborting now" exit | ||||
|     fi | ||||
|  | ||||
|     if [ -z "$update" ]; then echo -e "You didn't enter anything, aborting now." exit | ||||
|     fi | ||||
|  | ||||
|     if [ "$update" = Y ] || [ "$update" = y ]; then | ||||
|         # Updating and upgrading the system, without taking too much space | ||||
|         echo -e "\e[1;36m"Running apt-get update and apt-get dist-upgrade for you..."\e[0m" | ||||
| 	sleep 1 | ||||
|         echo -e "\e[1;36m"This will take a while, sometimes up to 1 hour"\e[0m" | ||||
|         sudo apt-get update && sudo apt-get dist-upgrade -y && sudo apt-get clean | ||||
|         echo -e "\e[1;36m"System successfully updated and upgraded!"\e[0m" | ||||
|         echo "" | ||||
|     fi | ||||
|  | ||||
|     # Cloning Inky-Calendar repo | ||||
|     echo -e "\e[1;36m"Cloning Inky-Calendar repo from Github"\e[0m" | ||||
|     cd /home/"$USER" && git clone https://github.com/aceisace/Inky-Calendar | ||||
|  | ||||
|     # Installing dependencies | ||||
|     echo -e "\e[1;36m"Installing requirements for Inky-Calendar software"\e[0m" | ||||
|     cd /home/"$USER"/Inky-Calendar && pip3 install -r requirements.txt | ||||
|  | ||||
|     # Create symlinks of settings and configuration file | ||||
|     ln -s /home/"$USER"/Inky-Calendar/settings/settings.py /home/"$USER"/Inky-Calendar/modules/ | ||||
|     ln -s /home/"$USER"/Inky-Calendar/settings/configuration.py /home/"$USER"/Inky-Calendar/modules/ | ||||
|     echo "" | ||||
|  | ||||
|     echo -e "\e[97mDo you want the software to start automatically at boot?" | ||||
|     echo -e "\e[97mPress [Y] for yes or [N] for no. The default option is yes" | ||||
| 	echo -e "\e[97mConfirm your selection with [ENTER]" | ||||
| 	read -r -p 'Waiting for input...  ' autostart | ||||
|  | ||||
| 	if [ "$autostart" != Y ] && [ "$autostart" != y ] && [ "$autostart" != N ] && [ "$autostart" != n ]; then echo -e "invalid input, aborting now" exit | ||||
|     fi | ||||
|  | ||||
|     if [ -z "$autostart" ] || [ "$autostart" = Y ] || [ "$autostart" = y ]; then | ||||
| 	    # Setting up supervisor | ||||
| 	    echo -e "\e[1;36m"Setting up auto-start of script at boot"\e[0m" | ||||
| 	    sudo apt-get install supervisor -y | ||||
|  | ||||
| 	    sudo bash -c 'cat > /etc/supervisor/conf.d/inkycal.conf' << EOF | ||||
| [program:Inky-Calendar] | ||||
| command = /usr/bin/python3 /home/$USER/Inky-Calendar/modules/inkycal.py | ||||
| stdout_logfile = /home/$USER/Inky-Calendar/logs/logfile.log | ||||
| stdout_logfile_maxbytes = 5MB | ||||
| stderr_logfile = /home/$USER/Inky-Calendar/logs/errors.log | ||||
| stderr_logfile_maxbytes = 5MB | ||||
| user = $USER | ||||
| startsecs = 30 | ||||
| EOF | ||||
|  | ||||
| 	    sudo service supervisor reload && sudo service supervisor start Inky-Calendar | ||||
| 	    echo "" | ||||
| 	fi | ||||
|  | ||||
|     # Final words | ||||
|     echo -e "\e[1;36m"The install was successful."\e[0m" | ||||
|     sleep 2 | ||||
|     echo -e "\e[1;31m"You can now add your personal details in the settings file"\e[0m" | ||||
|     echo -e "\e[1;31m"located in Inky-Calendar/settings/settings.py"\e[0m" | ||||
|     sleep 2 | ||||
|  | ||||
|     echo -e "\e[97mIf you want to add your details now, selet an option from below" | ||||
|     echo -e "\e[97mType [1] to open the settings-web-UI (user-fiendly)" | ||||
|     echo -e "\e[97mType [2] to open settings file with nano (can be run on SSH)" | ||||
|     echo -e "\e[97mType [3] to open settings file with python3 (can be run on SSH)" | ||||
|     echo -e "\e[97mLeave empty to skip this step" | ||||
| 	echo -e "\e[97mConfirm your selection with [ENTER]" | ||||
| 	read -r -p 'Waiting for input...  ' settings | ||||
|  | ||||
| 	# Invalid number selected, abort | ||||
| 	if [ "$settings" != 1 ] && [ "$settings" != 2 ] && [ "$settings" != 3 ]; then echo -e "invalid number, skipping.." | ||||
| 	fi | ||||
|  | ||||
| 	# No option selected, abort | ||||
| 	if [ -z "$settings" ]; then echo -e "You didn't enter anything, skipping.." | ||||
| 	fi | ||||
|  | ||||
| 	# What to do when uninstalling software | ||||
| 	if [ "$settings" = 1 ]; then | ||||
| 		echo -e "\e[1;36m"Add your details, click on generate, keep the file and close the browser"\e[0m" | ||||
| 		sleep 5 | ||||
| 		chromium-browser /home/"$USER"/Inky-Calendar/settings/settings-UI.html | ||||
| 		echo -e "\e[97mHave you added your details and clicked on 'Generate'?" | ||||
| 		echo -e "\e[97mPress [Y] for yes." | ||||
| 		read -r -p 'Waiting for input...  ' complete | ||||
| 		if [ -z "$complete" ] || [ "$complete" = Y ] || [ "$complete" = y ]; then | ||||
| 			echo -e "\e[1;36m"Moving settings file to /home/"$USER"/Inky-Calendar/settings/"\e[0m" | ||||
| 			if [ -e /etc/supervisor/conf.d/inkycal.conf ]; then mv /home/"$USER"/Downloads/settings.py /home/"$USER"/Inky-Calendar/settings/ | ||||
|     		fi | ||||
| 		fi | ||||
| 	fi | ||||
|  | ||||
| 	if [ "$settings" = 2 ]; then | ||||
| 		echo -e "\e[1;36m"Opening settings file with nano"\e[0m" | ||||
| 		nano /home/"$USER"/Inky-Calendar/settings/settings.py | ||||
| 	fi | ||||
|  | ||||
| 	if [ "$settings" = 3 ]; then | ||||
| 		echo -e "\e[1;36m"Opening settings file with python3"\e[0m" | ||||
| 		python3 /home/"$USER"/Inky-Calendar/settings/settings.py | ||||
| 	fi | ||||
|  | ||||
|     echo -e "\e[1;36m"You can test if the programm works by running:"\e[0m" | ||||
|     echo -e "\e[1;36m"python3 /home/"$USER"/Inky-Calendar/modules/inkycal.py"\e[0m" | ||||
| fi | ||||
| @@ -1,15 +1,17 @@ | ||||
| # Settings and Layout | ||||
| from inkycal.config.layout import Layout | ||||
| from inkycal.config.settings_parser import Settings | ||||
| #from inkycal.config.layout import Layout | ||||
| #from inkycal.config.settings_parser import Settings | ||||
| from inkycal.display import Display | ||||
|  | ||||
| # All supported inkycal_modules | ||||
| import inkycal.modules.inkycal_agenda | ||||
| import inkycal.modules.inkycal_calendar | ||||
| import inkycal.modules.inkycal_weather | ||||
| import inkycal.modules.inkycal_rss | ||||
| # import inkycal.modules.inkycal_image | ||||
| #import inkycal.modules.inkycal_image | ||||
| # import inkycal.modules.inkycal_server | ||||
|  | ||||
| # Main file | ||||
| from inkycal.main import Inkycal | ||||
|  | ||||
| # Added by module adder  | ||||
|   | ||||
							
								
								
									
										474
									
								
								inkycal/backup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								inkycal/backup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,474 @@ | ||||
| from inkycal import Settings, Layout | ||||
| from inkycal.custom import * | ||||
|  | ||||
| #from os.path import exists | ||||
| import os | ||||
| import traceback | ||||
| import logging | ||||
| import arrow | ||||
| import time | ||||
|  | ||||
| try: | ||||
|   from PIL import Image | ||||
| except ImportError: | ||||
|   print('Pillow is not installed! Please install with:') | ||||
|   print('pip3 install Pillow') | ||||
|  | ||||
| try: | ||||
|   import numpy | ||||
| except ImportError: | ||||
|   print('numpy is not installed! Please install with:') | ||||
|   print('pip3 install numpy') | ||||
|  | ||||
| logger = logging.getLogger('inkycal') | ||||
| logger.setLevel(level=logging.ERROR) | ||||
|  | ||||
| class Inkycal: | ||||
|   """Inkycal main class""" | ||||
|  | ||||
|   def __init__(self, settings_path, render=True): | ||||
|     """Initialise Inkycal | ||||
|     settings_path = str -> location/folder of settings file | ||||
|     render = bool -> show something on the ePaper? | ||||
|     """ | ||||
|     self._release = '2.0.0beta' | ||||
|  | ||||
|     # Check if render is boolean | ||||
|     if not isinstance(render, bool): | ||||
|       raise Exception('render must be True or False, not "{}"'.format(render)) | ||||
|     self.render = render | ||||
|  | ||||
|     # Init settings class | ||||
|     self.Settings = Settings(settings_path) | ||||
|  | ||||
|     # Check if display support colour | ||||
|     self.supports_colour = self.Settings.Layout.supports_colour | ||||
|  | ||||
|     # Option to flip image upside down | ||||
|     if self.Settings.display_orientation == 'normal': | ||||
|       self.upside_down = False | ||||
|  | ||||
|     elif self.Settings.display_orientation == 'upside_down': | ||||
|       self.upside_down = True | ||||
|  | ||||
|     # Option to use epaper image optimisation | ||||
|     self.optimize = True | ||||
|  | ||||
|     # Load drivers if image should be rendered | ||||
|     if self.render == True: | ||||
|  | ||||
|       # Get model and check if colour can be rendered | ||||
|       model= self.Settings.model | ||||
|  | ||||
|       # Init Display class | ||||
|       from inkycal.display import Display | ||||
|       self.Display = Display(model) | ||||
|  | ||||
|       # get calibration hours | ||||
|       self._calibration_hours = self.Settings.calibration_hours | ||||
|  | ||||
|       # set a check for calibration | ||||
|       self._calibration_state = False | ||||
|  | ||||
|     # load+validate settings file. Import and setup specified modules | ||||
|     self.active_modules = self.Settings.active_modules() | ||||
|     for module in self.active_modules: | ||||
|       try: | ||||
|         loader = 'from inkycal.modules import {0}'.format(module) | ||||
|         module_data = self.Settings.get_config(module) | ||||
|         size, conf = module_data['size'], module_data['config'] | ||||
|         setup = 'self.{} = {}(size, conf)'.format(module, module) | ||||
|         exec(loader) | ||||
|         exec(setup) | ||||
|         logger.debug(('{}: size: {}, config: {}'.format(module, size, conf))) | ||||
|  | ||||
|       # If a module was not found, print an error message | ||||
|       except ImportError: | ||||
|         print( | ||||
|           'Could not find module: "{}". Please try to import manually.'.format( | ||||
|           module)) | ||||
|  | ||||
|     # Give an OK message | ||||
|     print('loaded inkycal') | ||||
|  | ||||
|   def countdown(self, interval_mins=None): | ||||
|     """Returns the remaining time in seconds until next display update""" | ||||
|  | ||||
|     # Validate update interval | ||||
|     allowed_intervals = [10, 15, 20, 30, 60] | ||||
|  | ||||
|     # Check if empty, if empty, use value from settings file | ||||
|     if interval_mins == None: | ||||
|       interval_mins = self.Settings.update_interval | ||||
|  | ||||
|     # Check if integer | ||||
|     if not isinstance(interval_mins, int): | ||||
|       raise Exception('Update interval must be an integer -> 60') | ||||
|  | ||||
|     # Check if value is supported | ||||
|     if interval_mins not in allowed_intervals: | ||||
|       raise Exception('Update interval is {}, but should be one of: {}'.format( | ||||
|         interval_mins, allowed_intervals)) | ||||
|  | ||||
|     # Find out at which minutes the update should happen | ||||
|     now = arrow.now() | ||||
|     update_timings = [(60 - int(interval_mins)*updates) for updates in | ||||
|                       range(60//int(interval_mins))][::-1] | ||||
|  | ||||
|     # Calculate time in mins until next update | ||||
|     minutes = [_ for _ in update_timings if _>= now.minute][0] - now.minute | ||||
|  | ||||
|     # Print the remaining time in mins until next update | ||||
|     print('{0} Minutes left until next refresh'.format(minutes)) | ||||
|  | ||||
|     # Calculate time in seconds until next update | ||||
|     remaining_time = minutes*60 + (60 - now.second) | ||||
|  | ||||
|     # Return seconds until next update | ||||
|     return remaining_time | ||||
|  | ||||
|   def test(self): | ||||
|     """Inkycal test run. | ||||
|     Generates images for each module, one by one and prints OK if no | ||||
|     problems were found.""" | ||||
|     print('You are running inkycal v{}'.format(self._release)) | ||||
|  | ||||
|  | ||||
|     print('Running inkycal test-run for {} ePaper'.format( | ||||
|       self.Settings.model)) | ||||
|  | ||||
|     if self.upside_down == True: | ||||
|       print('upside-down mode active') | ||||
|  | ||||
|     for module in self.active_modules: | ||||
|       generate_im = 'self.{0}.generate_image()'.format(module) | ||||
|       print('generating image for {} module...'.format(module), end = '') | ||||
|       try: | ||||
|         exec(generate_im) | ||||
|         print('OK!') | ||||
|       except Exception as Error: | ||||
|         print('Error!') | ||||
|         print(traceback.format_exc()) | ||||
|  | ||||
|   def run(self): | ||||
|     """Runs the main inykcal program nonstop (cannot be stopped anymore!) | ||||
|     Will show something on the display if render was set to True""" | ||||
|  | ||||
|     # TODO: printing traceback on display (or at least a smaller message?) | ||||
|     # Calibration | ||||
|  | ||||
|     # Get the time of initial run | ||||
|     runtime = arrow.now() | ||||
|  | ||||
|     # Function to flip images upside down | ||||
|     upside_down = lambda image: image.rotate(180, expand=True) | ||||
|  | ||||
|     # Count the number of times without any errors | ||||
|     counter = 1 | ||||
|  | ||||
|     # Calculate the max. fontsize for info-section | ||||
|     if self.Settings.info_section == True: | ||||
|       info_section_height = round(self.Settings.Layout.display_height* (1/95) ) | ||||
|       self.font = auto_fontsize(ImageFont.truetype( | ||||
|         fonts['NotoSans-SemiCondensed']), info_section_height) | ||||
|  | ||||
|     while True: | ||||
|       print('Generating images for all modules...') | ||||
|       for module in self.active_modules: | ||||
|         generate_im = 'self.{0}.generate_image()'.format(module) | ||||
|         try: | ||||
|           exec(generate_im) | ||||
|         except Exception as Error: | ||||
|           print('Error!') | ||||
|           message = traceback.format_exc() | ||||
|           print(message) | ||||
|           counter = 0 | ||||
|       print('OK') | ||||
|  | ||||
|       # Assemble image from each module | ||||
|       self._assemble() | ||||
|  | ||||
|       # Check if image should be rendered | ||||
|       if self.render == True: | ||||
|         Display = self.Display | ||||
|  | ||||
|         self._calibration_check() | ||||
|  | ||||
|         if self.supports_colour == True: | ||||
|           im_black = Image.open(images+'canvas.png') | ||||
|           im_colour = Image.open(images+'canvas_colour.png') | ||||
|  | ||||
|           # Flip the image by 180° if required | ||||
|           if self.upside_down == True: | ||||
|             im_black = upside_down(im_black) | ||||
|             im_colour = upside_down(im_colour) | ||||
|  | ||||
|           # render the image on the display | ||||
|           Display.render(im_black, im_colour) | ||||
|  | ||||
|         # Part for black-white ePapers | ||||
|         elif self.supports_colour == False: | ||||
|  | ||||
|           im_black = self._merge_bands() | ||||
|  | ||||
|           # Flip the image by 180° if required | ||||
|           if self.upside_down == True: | ||||
|             im_black = upside_down(im_black) | ||||
|  | ||||
|           Display.render(im_black) | ||||
|  | ||||
|       print('\ninkycal has been running without any errors for', end = ' ') | ||||
|       print('{} display updates'.format(counter)) | ||||
|       print('Programm started {}'.format(runtime.humanize())) | ||||
|  | ||||
|       counter += 1 | ||||
|  | ||||
|       sleep_time = self.countdown() | ||||
|       time.sleep(sleep_time) | ||||
|  | ||||
|   def _merge_bands(self): | ||||
|     """Merges black and coloured bands for black-white ePapers | ||||
|     returns the merged image | ||||
|     """ | ||||
|  | ||||
|     im_path = images | ||||
|  | ||||
|     im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png' | ||||
|  | ||||
|     # If there is an image for black and colour, merge them | ||||
|     if os.path.exists(im1_path) and os.path.exists(im2_path): | ||||
|  | ||||
|       im1 = Image.open(im1_path).convert('RGBA') | ||||
|       im2 = Image.open(im2_path).convert('RGBA') | ||||
|  | ||||
|       def clear_white(img): | ||||
|         """Replace all white pixels from image with transparent pixels | ||||
|         """ | ||||
|         x = numpy.asarray(img.convert('RGBA')).copy() | ||||
|         x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8) | ||||
|         return Image.fromarray(x) | ||||
|  | ||||
|       im2 = clear_white(im2) | ||||
|       im1.paste(im2, (0,0), im2) | ||||
|  | ||||
|     # If there is no image for the coloured-band, return the bw-image | ||||
|     elif os.path.exists(im1_path) and not os.path.exists(im2_path): | ||||
|       im1 = Image.open(im1_name).convert('RGBA') | ||||
|  | ||||
|     return im1 | ||||
|  | ||||
|  | ||||
|   def _assemble(self): | ||||
|     """Assmebles all sub-images to a single image""" | ||||
|  | ||||
|     # Create an empty canvas with the size of the display | ||||
|     width, height = self.Settings.Layout.display_size | ||||
|      | ||||
|     if self.Settings.info_section == True: | ||||
|       height = round(height * ((1/95)*100) ) | ||||
|  | ||||
|     im_black = Image.new('RGB', (width, height), color = 'white') | ||||
|     im_colour = Image.new('RGB', (width ,height), color = 'white') | ||||
|  | ||||
|     # Set cursor for y-axis | ||||
|     im1_cursor = 0 | ||||
|     im2_cursor = 0 | ||||
|  | ||||
|     for module in self.active_modules: | ||||
|  | ||||
|       im1_path = images+module+'.png' | ||||
|       im2_path = images+module+'_colour.png' | ||||
|  | ||||
|       # Check if there is an image for the black band | ||||
|       if os.path.exists(im1_path): | ||||
|  | ||||
|         # Get actual size of image | ||||
|         im1 = Image.open(im1_path).convert('RGBA') | ||||
|         im1_size = im1.size | ||||
|  | ||||
|         # Get the size of the section | ||||
|         section_size = self.Settings.get_config(module)['size'] | ||||
|         # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0] - im1_size[0]) /2) | ||||
|  | ||||
|         # If this is the first module, use the y-offset | ||||
|         if im1_cursor == 0: | ||||
|           y = int( (section_size[1]-im1_size[1]) /2) | ||||
|         else: | ||||
|           y = im1_cursor + int( (section_size[1]-im1_size[1]) /2) | ||||
|  | ||||
|         # center the image in the section space | ||||
|         im_black.paste(im1, (x,y), im1) | ||||
|  | ||||
|         # Shift the y-axis cursor at the beginning of next section | ||||
|         im1_cursor += section_size[1] | ||||
|  | ||||
|       # Check if there is an image for the coloured band | ||||
|       if os.path.exists(im2_path): | ||||
|  | ||||
|         # Get actual size of image | ||||
|         im2 = Image.open(im2_path).convert('RGBA') | ||||
|         im2_size = im2.size | ||||
|  | ||||
|         # Get the size of the section | ||||
|         section_size = self.Settings.get_config(module)['size'] | ||||
|  | ||||
|         # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0]-im2_size[0]) /2) | ||||
|  | ||||
|         # If this is the first module, use the y-offset | ||||
|         if im2_cursor == 0: | ||||
|           y = int( (section_size[1]-im2_size[1]) /2) | ||||
|         else: | ||||
|           y = im2_cursor + int( (section_size[1]-im2_size[1]) /2) | ||||
|  | ||||
|         # center the image in the section space | ||||
|         im_colour.paste(im2, (x,y), im2) | ||||
|  | ||||
|         # Shift the y-axis cursor at the beginning of next section | ||||
|         im2_cursor += section_size[1] | ||||
|  | ||||
|     # Show an info section if specified by the settings file | ||||
|     now = arrow.now() | ||||
|     stamp = 'last update: {}'.format(now.format('D MMM @ HH:mm', locale = | ||||
|                                                 self.Settings.language)) | ||||
|     if self.Settings.info_section == True: | ||||
|       write(im_black, (0, im1_cursor), (width, height-im1_cursor), | ||||
|             stamp, font = self.font) | ||||
|  | ||||
|  | ||||
|     # optimize the image by mapping colours to pure black and white | ||||
|     if self.optimize == True: | ||||
|       self._optimize_im(im_black).save(images+'canvas.png', 'PNG') | ||||
|       self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG') | ||||
|     else: | ||||
|       im_black.save(images+'canvas.png', 'PNG') | ||||
|       im_colour.save(images+'canvas_colour.png', 'PNG') | ||||
|  | ||||
|   def _optimize_im(self, image, threshold=220): | ||||
|     """Optimize the image for rendering on ePaper displays""" | ||||
|  | ||||
|     buffer = numpy.array(image.convert('RGB')) | ||||
|     red, green = buffer[:, :, 0], buffer[:, :, 1] | ||||
|     # grey->black | ||||
|     buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] | ||||
|     image = Image.fromarray(buffer) | ||||
|     return image | ||||
|  | ||||
|   def calibrate(self): | ||||
|     """Calibrate the ePaper display to prevent burn-ins (ghosting) | ||||
|     use this command to manually calibrate the display""" | ||||
|  | ||||
|     self.Display.calibrate() | ||||
|  | ||||
|   def _calibration_check(self): | ||||
|     """Calibration sheduler | ||||
|     uses calibration hours from settings file to check if calibration is due""" | ||||
|     now = arrow.now() | ||||
|     print('hour:', now.hour, 'hours:', self._calibration_hours) | ||||
|     print('state:', self._calibration_state) | ||||
|     if now.hour in self._calibration_hours and self._calibration_state == False: | ||||
|       self.calibrate() | ||||
|       self._calibration_state = True | ||||
|     else: | ||||
|       self._calibration_state = False | ||||
|  | ||||
|  | ||||
|   def _check_for_updates(self): | ||||
|     """Check if a new update is available for inkycal""" | ||||
|  | ||||
|     raise NotImplementedError('Tha developer were too lazy to implement this..') | ||||
|  | ||||
|  | ||||
|   @staticmethod | ||||
|   def _add_module(filepath_module, classname): | ||||
|     """Add a third party module to inkycal | ||||
|     filepath_module = the full path of your module. The file should be in /modules! | ||||
|     classname = the name of your class inside the module | ||||
|     """ | ||||
|  | ||||
|     # Path for modules | ||||
|     _module_path = 'inkycal/modules/' | ||||
|  | ||||
|     # Check if the filepath is a string | ||||
|     if not isinstance(filepath_module, str): | ||||
|       raise ValueError('filepath has to be a string!') | ||||
|  | ||||
|     # Check if the classname is a string | ||||
|     if not isinstance(classname, str): | ||||
|       raise ValueError('classname has to be a string!') | ||||
|  | ||||
|     # TODO: | ||||
|     # Ensure only third-party modules are deleted as built-in modules | ||||
|     # should not be deleted | ||||
|  | ||||
|     # Check if module is inside the modules folder | ||||
|     if not _module_path in filepath_module: | ||||
|       raise Exception('Your module should be in', _module_path) | ||||
|  | ||||
|     # Get the name of the third-party module file without extension (.py) | ||||
|     filename = filepath_module.split('.py')[0].split('/')[-1] | ||||
|  | ||||
|     # Check if filename or classname is in the current module init file | ||||
|     with open('modules/__init__.py', mode ='r') as module_init: | ||||
|       content = module_init.read().splitlines() | ||||
|  | ||||
|     for line in content: | ||||
|       if (filename or clasname) in line: | ||||
|         raise Exception( | ||||
|           'A module with this filename or classname already exists') | ||||
|  | ||||
|     # Check if filename or classname is in the current inkycal init file | ||||
|     with open('__init__.py', mode ='r') as inkycal_init: | ||||
|       content = inkycal_init.read().splitlines() | ||||
|  | ||||
|     for line in content: | ||||
|       if (filename or clasname) in line: | ||||
|         raise Exception( | ||||
|           'A module with this filename or classname already exists') | ||||
|  | ||||
|     # If all checks have passed, add the module in the module init file | ||||
|     with open('modules/__init__.py', mode='a') as module_init: | ||||
|       module_init.write('from .{} import {}'.format(filename, classname)) | ||||
|  | ||||
|     # If all checks have passed, add the module in the inkycal init file | ||||
|     with open('__init__.py', mode ='a') as inkycal_init: | ||||
|       inkycal_init.write('# Added by module adder \n') | ||||
|       inkycal_init.write('import inkycal.modules.{}'.format(filename)) | ||||
|  | ||||
|     print('Your module {} has been added successfully! Hooray!'.format( | ||||
|       classname)) | ||||
|  | ||||
|   @staticmethod | ||||
|   def _remove_module(classname, remove_file = True): | ||||
|     """Removes a third-party module from inkycal | ||||
|     Input the classname of the file you want to remove  | ||||
|     """ | ||||
|  | ||||
|     # Check if filename or classname is in the current module init file | ||||
|     with open('modules/__init__.py', mode ='r') as module_init: | ||||
|       content = module_init.read().splitlines() | ||||
|  | ||||
|     with open('modules/__init__.py', mode ='w') as module_init: | ||||
|       for line in content: | ||||
|         if not classname in line: | ||||
|           module_init.write(line+'\n') | ||||
|         else: | ||||
|           filename = line.split(' ')[1].split('.')[1] | ||||
|  | ||||
|     # Check if filename or classname is in the current inkycal init file | ||||
|     with open('__init__.py', mode ='r') as inkycal_init: | ||||
|       content = inkycal_init.read().splitlines() | ||||
|  | ||||
|     with open('__init__.py', mode ='w') as inkycal_init: | ||||
|       for line in content: | ||||
|         if not filename in line: | ||||
|           inkycal_init.write(line+'\n') | ||||
|  | ||||
|     # remove the file of the third party module if it exists and remove_file | ||||
|     # was set to True (default) | ||||
|     if os.path.exists('modules/{}.py'.format(filename)) and remove_file == True: | ||||
|       os.remove('modules/{}.py'.format(filename)) | ||||
|  | ||||
|     print('The module {} has been removed successfully'.format(classname)) | ||||
|  | ||||
| @@ -24,8 +24,6 @@ class Layout: | ||||
|     if (model != None) and (width == None) and (height == None): | ||||
|       display_dimensions = { | ||||
|         '9_in_7': (1200, 825), | ||||
|         'epd_7_in_5_v3_colour': (880, 528), | ||||
|         'epd_7_in_5_v3': (880, 528), | ||||
|         'epd_7_in_5_v2_colour': (800, 480), | ||||
|         'epd_7_in_5_v2': (800, 480), | ||||
|         'epd_7_in_5_colour': (640, 384), | ||||
| @@ -116,6 +114,14 @@ class Layout: | ||||
|         size = (self.bottom_section_width, self.bottom_section_height) | ||||
|       return size | ||||
|  | ||||
| ##  def set_info_section(self, value): | ||||
| ##    """Should a small info section be showed """ | ||||
| ##    if not isinstance(value, bool): | ||||
| ##      raise ValueError('value has to bee a boolean: True/False') | ||||
| ##    self.info_section = value | ||||
| ##    logger.info(('show info section: {}').format(value)) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone/debug mode'.format( | ||||
|     os.path.basename(__file__).split('.py')[0])) | ||||
|   | ||||
| @@ -23,7 +23,6 @@ class Settings: | ||||
|   _supported_update_interval = [10, 15, 20, 30, 60] | ||||
|   _supported_display_orientation = ['normal', 'upside_down'] | ||||
|   _supported_models = [ | ||||
|   'epd_7_in_5_v3_colour', 'epd_7_in_5_v3', | ||||
|   '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', | ||||
|   | ||||
| @@ -67,7 +67,7 @@ def auto_fontsize(font, max_height): | ||||
| 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) | ||||
|   xy = (x,y) coordinates as tuple -> (x,y) | ||||
|   box_size = size of text-box -> (width,height) | ||||
|   text = string (what to write) | ||||
|   font = which font to use | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| from .epaper import Display | ||||
| from .display import Display | ||||
|   | ||||
							
								
								
									
										130
									
								
								inkycal/display/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								inkycal/display/display.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| #!/usr/bin/python3 | ||||
| # -*- coding: utf-8 -*- | ||||
| """ | ||||
| Inky-Calendar epaper functions | ||||
| Copyright by aceisace | ||||
| """ | ||||
| from importlib import import_module | ||||
| from PIL import Image | ||||
|  | ||||
| from inkycal.custom import top_level | ||||
| import glob | ||||
|  | ||||
| class Display: | ||||
|   """Display class for inkycal | ||||
|   Handles rendering on display""" | ||||
|  | ||||
|   def __init__(self, epaper_model): | ||||
|     """Load the drivers for this epaper model""" | ||||
|  | ||||
|     if 'colour' in epaper_model: | ||||
|       self.supports_colour = True | ||||
|     else: | ||||
|       self.supports_colour = False | ||||
|  | ||||
|     try: | ||||
|       driver_path = f'inkycal.display.drivers.{epaper_model}' | ||||
|       driver = import_module(driver_path) | ||||
|       self._epaper = driver.EPD() | ||||
|       self.model_name = epaper_model | ||||
|       #self.height = driver.EPD_HEIGHT | ||||
|       #self.width = driver.EPD_WIDTH | ||||
|  | ||||
|     except ImportError: | ||||
|       raise Exception('This module is not supported. Check your spellings?') | ||||
|  | ||||
|     except FileNotFoundError: | ||||
|       raise Exception('SPI could not be found. Please check if SPI is enabled') | ||||
|  | ||||
|   def render(self, im_black, im_colour = None): | ||||
|     """Render an image on the epaper | ||||
|     im_colour is required for three-colour epapers""" | ||||
|  | ||||
|     epaper = self._epaper | ||||
|  | ||||
|     if self.supports_colour == False: | ||||
|       print('Initialising..', end = '') | ||||
|       epaper.init() | ||||
|       # For the 9.7" ePaper, the image needs to be flipped by 90 deg first | ||||
|       # The other displays flip the image automatically | ||||
|       if self.model_name == "9_in_7": | ||||
|         im_black.rotate(90, expand=True) | ||||
|       print('Updating display......', end = '') | ||||
|       epaper.display(epaper.getbuffer(im_black)) | ||||
|       print('Done') | ||||
|  | ||||
|     elif self.supports_colour == True: | ||||
|       if not im_colour: | ||||
|         raise Exception('im_colour is required for coloured epaper displays') | ||||
|       print('Initialising..', end = '') | ||||
|       epaper.init() | ||||
|       print('Updating display......', end = '') | ||||
|       epaper.display(epaper.getbuffer(im_black), epaper.getbuffer(im_colour)) | ||||
|       print('Done') | ||||
|  | ||||
|     print('Sending E-Paper to deep sleep...', end = '') | ||||
|     epaper.sleep() | ||||
|     print('Done') | ||||
|  | ||||
|   def calibrate(self, cycles=3): | ||||
|     """Flush display with single colour to prevent burn-ins (ghosting) | ||||
|     cycles -> int. How many times should each colour be flushed? | ||||
|     recommended cycles = 3""" | ||||
|  | ||||
|     epaper = self._epaper | ||||
|     epaper.init() | ||||
|  | ||||
|     white = Image.new('1', (epaper.width, epaper.height), 'white') | ||||
|     black = Image.new('1', (epaper.width, epaper.height), 'black') | ||||
|  | ||||
|     print('----------Started calibration of ePaper display----------') | ||||
|     if self.supports_colour == True: | ||||
|       for _ in range(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, cycles)) | ||||
|  | ||||
|     if self.supports_colour == False: | ||||
|       for _ in range(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, cycles)) | ||||
|  | ||||
|       print('-----------Calibration complete----------') | ||||
|       epaper.sleep() | ||||
|  | ||||
|  | ||||
|   @classmethod | ||||
|   def get_display_size(cls, model_name): | ||||
|     "returns (width, height) of given display" | ||||
|     if not isinstance(model_name, str): | ||||
|       print('model_name should be a string') | ||||
|       return | ||||
|     else: | ||||
|       driver_files = top_level+'/inkycal/display/drivers/*.py' | ||||
|       drivers = glob.glob(driver_files) | ||||
|       drivers = [i.split('/')[-1].split('.')[0] for i in drivers] | ||||
|       if model_name not in drivers: | ||||
|         print('This model name was not found. Please double check your spellings') | ||||
|         return | ||||
|       else: | ||||
|         with open(top_level+'/inkycal/display/drivers/'+model_name+'.py') as file: | ||||
|           for line in file: | ||||
|             if 'EPD_WIDTH=' in line.replace(" ", ""): | ||||
|               width = int(line.rstrip().replace(" ", "").split('=')[-1]) | ||||
|             if 'EPD_HEIGHT=' in line.replace(" ", ""): | ||||
|               height = int(line.rstrip().replace(" ", "").split('=')[-1]) | ||||
|         return width, height | ||||
|  | ||||
|              | ||||
| if __name__ == '__main__': | ||||
|   print("Running Display class in standalone mode") | ||||
|    | ||||
							
								
								
									
										413
									
								
								inkycal/main.py
									
									
									
									
									
								
							
							
						
						
									
										413
									
								
								inkycal/main.py
									
									
									
									
									
								
							| @@ -1,12 +1,19 @@ | ||||
| from inkycal import Settings, Layout | ||||
| from inkycal.custom import * | ||||
| #!/usr/bin/python3 | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| #from os.path import exists | ||||
| """ | ||||
| Main class for inkycal Project | ||||
| Copyright by aceisace | ||||
| """ | ||||
|  | ||||
| from inkycal.display import Display | ||||
| from inkycal.custom import * | ||||
| import os | ||||
| import traceback | ||||
| import logging | ||||
| import arrow | ||||
| import time | ||||
| import json | ||||
|  | ||||
| try: | ||||
|   from PIL import Image | ||||
| @@ -20,36 +27,36 @@ except ImportError: | ||||
|   print('numpy is not installed! Please install with:') | ||||
|   print('pip3 install numpy') | ||||
|  | ||||
| logger = logging.getLogger('inkycal') | ||||
| filename = os.path.basename(__file__).split('.py')[0] | ||||
| logger = logging.getLogger(filename) | ||||
| logger.setLevel(level=logging.ERROR) | ||||
|  | ||||
|  | ||||
| class Inkycal: | ||||
|   """Inkycal main class""" | ||||
|  | ||||
|   def __init__(self, settings_path, render=True): | ||||
|     """initialise class | ||||
|     """Initialise Inkycal | ||||
|     settings_path = str -> location/folder of settings file | ||||
|     render = bool -> show something on the ePaper? | ||||
|     """ | ||||
|     self._release = '2.0.0beta' | ||||
|     self._release = '2.0.0' | ||||
|  | ||||
|     # Check if render is boolean | ||||
|     if not isinstance(render, bool): | ||||
|     # Check if render was set correctly | ||||
|     if render not in [True, False]: | ||||
|       raise Exception('render must be True or False, not "{}"'.format(render)) | ||||
|     self.render = render | ||||
|  | ||||
|     # Init settings class | ||||
|     self.Settings = Settings(settings_path) | ||||
|     # load settings file - throw an error if file could not be found | ||||
|     try: | ||||
|       with open(settings_path) as file: | ||||
|         settings = json.load(file) | ||||
|         self.settings = settings | ||||
|         #print(self.settings) | ||||
|  | ||||
|     # Check if display support colour | ||||
|     self.supports_colour = self.Settings.Layout.supports_colour | ||||
|  | ||||
|     # Option to flip image upside down | ||||
|     if self.Settings.display_orientation == 'normal': | ||||
|       self.upside_down = False | ||||
|  | ||||
|     elif self.Settings.display_orientation == 'upside_down': | ||||
|       self.upside_down = True | ||||
|     except FileNotFoundError: | ||||
|       print('No settings file found in specified location') | ||||
|       print('Please double check your path') | ||||
|  | ||||
|     # Option to use epaper image optimisation | ||||
|     self.optimize = True | ||||
| @@ -57,27 +64,26 @@ class Inkycal: | ||||
|     # Load drivers if image should be rendered | ||||
|     if self.render == True: | ||||
|  | ||||
|       # Get model and check if colour can be rendered | ||||
|       model= self.Settings.model | ||||
|  | ||||
|       # Init Display class | ||||
|       # Init Display class with model in settings file | ||||
|       from inkycal.display import Display | ||||
|       self.Display = Display(model) | ||||
|       self.Display = Display(settings["model"]) | ||||
|  | ||||
|       # get calibration hours | ||||
|       self._calibration_hours = self.Settings.calibration_hours | ||||
|       # check if colours can be rendered | ||||
|       self.supports_colour = True if 'colour' in settings['model'] else False | ||||
|  | ||||
|       # set a check for calibration | ||||
|       # init calibration state | ||||
|       self._calibration_state = False | ||||
|  | ||||
|     # load+validate settings file. Import and setup specified modules | ||||
|     self.active_modules = self.Settings.active_modules() | ||||
|     for module in self.active_modules: | ||||
|  | ||||
|  | ||||
|     # WIP | ||||
|     for module in settings['modules']: | ||||
|       try: | ||||
|         loader = 'from inkycal.modules import {0}'.format(module) | ||||
|         module_data = self.Settings.get_config(module) | ||||
|         size, conf = module_data['size'], module_data['config'] | ||||
|         setup = 'self.{} = {}(size, conf)'.format(module, module) | ||||
|         loader = f'from inkycal.modules import {module["name"]}' | ||||
|         print(loader) | ||||
|         conf = module["config"] | ||||
|         #size, conf = module_data['size'], module_data['config'] | ||||
|         setup = f'self.{module} = {module}(size, conf)' | ||||
|         exec(loader) | ||||
|         exec(setup) | ||||
|         logger.debug(('{}: size: {}, config: {}'.format(module, size, conf))) | ||||
| @@ -88,6 +94,9 @@ class Inkycal: | ||||
|           'Could not find module: "{}". Please try to import manually.'.format( | ||||
|           module)) | ||||
|  | ||||
|       except Exception as e: | ||||
|         print(str(e)) | ||||
|  | ||||
|     # Give an OK message | ||||
|     print('loaded inkycal') | ||||
|  | ||||
| @@ -99,7 +108,7 @@ class Inkycal: | ||||
|  | ||||
|     # Check if empty, if empty, use value from settings file | ||||
|     if interval_mins == None: | ||||
|       interval_mins = self.Settings.update_interval | ||||
|       interval_mins = self.settings.update_interval | ||||
|  | ||||
|     # Check if integer | ||||
|     if not isinstance(interval_mins, int): | ||||
| @@ -127,348 +136,18 @@ class Inkycal: | ||||
|     # Return seconds until next update | ||||
|     return remaining_time | ||||
|  | ||||
|   def test(self): | ||||
|     """Inkycal test run. | ||||
|     Generates images for each module, one by one and prints OK if no | ||||
|     problems were found.""" | ||||
|     print('You are running inkycal v{}'.format(self._release)) | ||||
|  | ||||
|  | ||||
|     print('Running inkycal test-run for {} ePaper'.format( | ||||
|       self.Settings.model)) | ||||
|  | ||||
|     if self.upside_down == True: | ||||
|       print('upside-down mode active') | ||||
|  | ||||
|     for module in self.active_modules: | ||||
|       generate_im = 'self.{0}.generate_image()'.format(module) | ||||
|       print('generating image for {} module...'.format(module), end = '') | ||||
|       try: | ||||
|         exec(generate_im) | ||||
|         print('OK!') | ||||
|       except Exception as Error: | ||||
|         print('Error!') | ||||
|         print(traceback.format_exc()) | ||||
|  | ||||
|   def run(self): | ||||
|     """Runs the main inykcal program nonstop (cannot be stopped anymore!) | ||||
|     Will show something on the display if render was set to True""" | ||||
|  | ||||
|     # TODO: printing traceback on display (or at least a smaller message?) | ||||
|     # Calibration | ||||
|  | ||||
|     # Get the time of initial run | ||||
|     runtime = arrow.now() | ||||
|  | ||||
|     # Function to flip images upside down | ||||
|     upside_down = lambda image: image.rotate(180, expand=True) | ||||
|  | ||||
|     # Count the number of times without any errors | ||||
|     counter = 1 | ||||
|  | ||||
|     # Calculate the max. fontsize for info-section | ||||
|     if self.Settings.info_section == True: | ||||
|       info_section_height = round(self.Settings.Layout.display_height* (1/95) ) | ||||
|       self.font = auto_fontsize(ImageFont.truetype( | ||||
|         fonts['NotoSans-SemiCondensed']), info_section_height) | ||||
|  | ||||
|     while True: | ||||
|       print('Generating images for all modules...') | ||||
|       for module in self.active_modules: | ||||
|         generate_im = 'self.{0}.generate_image()'.format(module) | ||||
|         try: | ||||
|           exec(generate_im) | ||||
|         except Exception as Error: | ||||
|           print('Error!') | ||||
|           message = traceback.format_exc() | ||||
|           print(message) | ||||
|           counter = 0 | ||||
|       print('OK') | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone/debug mode'.format(filename)) | ||||
|  | ||||
|       # Assemble image from each module | ||||
|       self._assemble() | ||||
|  | ||||
|       # Check if image should be rendered | ||||
|       if self.render == True: | ||||
|         Display = self.Display | ||||
|  | ||||
|         self._calibration_check() | ||||
|  | ||||
|         if self.supports_colour == True: | ||||
|           im_black = Image.open(images+'canvas.png') | ||||
|           im_colour = Image.open(images+'canvas_colour.png') | ||||
|  | ||||
|           # Flip the image by 180° if required | ||||
|           if self.upside_down == True: | ||||
|             im_black = upside_down(im_black) | ||||
|             im_colour = upside_down(im_colour) | ||||
|  | ||||
|           # render the image on the display | ||||
|           Display.render(im_black, im_colour) | ||||
|  | ||||
|         # Part for black-white ePapers | ||||
|         elif self.supports_colour == False: | ||||
|  | ||||
|           im_black = self._merge_bands() | ||||
|  | ||||
|           # Flip the image by 180° if required | ||||
|           if self.upside_down == True: | ||||
|             im_black = upside_down(im_black) | ||||
|  | ||||
|           Display.render(im_black) | ||||
|  | ||||
|       print('\ninkycal has been running without any errors for', end = ' ') | ||||
|       print('{} display updates'.format(counter)) | ||||
|       print('Programm started {}'.format(runtime.humanize())) | ||||
|  | ||||
|       counter += 1 | ||||
|  | ||||
|       sleep_time = self.countdown() | ||||
|       time.sleep(sleep_time) | ||||
|  | ||||
|   def _merge_bands(self): | ||||
|     """Merges black and coloured bands for black-white ePapers | ||||
|     returns the merged image | ||||
|     """ | ||||
|  | ||||
|     im_path = images | ||||
|  | ||||
|     im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png' | ||||
|  | ||||
|     # If there is an image for black and colour, merge them | ||||
|     if os.path.exists(im1_path) and os.path.exists(im2_path): | ||||
|  | ||||
|       im1 = Image.open(im1_path).convert('RGBA') | ||||
|       im2 = Image.open(im2_path).convert('RGBA') | ||||
|  | ||||
|       def clear_white(img): | ||||
|         """Replace all white pixels from image with transparent pixels | ||||
|         """ | ||||
|         x = numpy.asarray(img.convert('RGBA')).copy() | ||||
|         x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8) | ||||
|         return Image.fromarray(x) | ||||
|  | ||||
|       im2 = clear_white(im2) | ||||
|       im1.paste(im2, (0,0), im2) | ||||
|  | ||||
|     # If there is no image for the coloured-band, return the bw-image | ||||
|     elif os.path.exists(im1_path) and not os.path.exists(im2_path): | ||||
|       im1 = Image.open(im1_name).convert('RGBA') | ||||
|  | ||||
|     return im1 | ||||
|  | ||||
|  | ||||
|   def _assemble(self): | ||||
|     """Assmebles all sub-images to a single image""" | ||||
|  | ||||
|     # Create an empty canvas with the size of the display | ||||
|     width, height = self.Settings.Layout.display_size | ||||
|      | ||||
|     if self.Settings.info_section == True: | ||||
|       height = round(height * ((1/95)*100) ) | ||||
|  | ||||
|     im_black = Image.new('RGB', (width, height), color = 'white') | ||||
|     im_colour = Image.new('RGB', (width ,height), color = 'white') | ||||
|  | ||||
|     # Set cursor for y-axis | ||||
|     im1_cursor = 0 | ||||
|     im2_cursor = 0 | ||||
|  | ||||
|     for module in self.active_modules: | ||||
|  | ||||
|       im1_path = images+module+'.png' | ||||
|       im2_path = images+module+'_colour.png' | ||||
|  | ||||
|       # Check if there is an image for the black band | ||||
|       if os.path.exists(im1_path): | ||||
|  | ||||
|         # Get actual size of image | ||||
|         im1 = Image.open(im1_path).convert('RGBA') | ||||
|         im1_size = im1.size | ||||
|  | ||||
|         # Get the size of the section | ||||
|         section_size = self.Settings.get_config(module)['size'] | ||||
|         # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0] - im1_size[0]) /2) | ||||
|  | ||||
|         # If this is the first module, use the y-offset | ||||
|         if im1_cursor == 0: | ||||
|           y = int( (section_size[1]-im1_size[1]) /2) | ||||
|         else: | ||||
|           y = im1_cursor + int( (section_size[1]-im1_size[1]) /2) | ||||
|  | ||||
|         # center the image in the section space | ||||
|         im_black.paste(im1, (x,y), im1) | ||||
|  | ||||
|         # Shift the y-axis cursor at the beginning of next section | ||||
|         im1_cursor += section_size[1] | ||||
|  | ||||
|       # Check if there is an image for the coloured band | ||||
|       if os.path.exists(im2_path): | ||||
|  | ||||
|         # Get actual size of image | ||||
|         im2 = Image.open(im2_path).convert('RGBA') | ||||
|         im2_size = im2.size | ||||
|  | ||||
|         # Get the size of the section | ||||
|         section_size = self.Settings.get_config(module)['size'] | ||||
|  | ||||
|         # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0]-im2_size[0]) /2) | ||||
|  | ||||
|         # If this is the first module, use the y-offset | ||||
|         if im2_cursor == 0: | ||||
|           y = int( (section_size[1]-im2_size[1]) /2) | ||||
|         else: | ||||
|           y = im2_cursor + int( (section_size[1]-im2_size[1]) /2) | ||||
|  | ||||
|         # center the image in the section space | ||||
|         im_colour.paste(im2, (x,y), im2) | ||||
|  | ||||
|         # Shift the y-axis cursor at the beginning of next section | ||||
|         im2_cursor += section_size[1] | ||||
|  | ||||
|     # Show an info section if specified by the settings file | ||||
|     now = arrow.now() | ||||
|     stamp = 'last update: {}'.format(now.format('D MMM @ HH:mm', locale = | ||||
|                                                 self.Settings.language)) | ||||
|     if self.Settings.info_section == True: | ||||
|       write(im_black, (0, im1_cursor), (width, height-im1_cursor), | ||||
|             stamp, font = self.font) | ||||
|  | ||||
|  | ||||
|     # optimize the image by mapping colours to pure black and white | ||||
|     if self.optimize == True: | ||||
|       self._optimize_im(im_black).save(images+'canvas.png', 'PNG') | ||||
|       self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG') | ||||
|     else: | ||||
|       im_black.save(images+'canvas.png', 'PNG') | ||||
|       im_colour.save(images+'canvas_colour.png', 'PNG') | ||||
|  | ||||
|   def _optimize_im(self, image, threshold=220): | ||||
|     """Optimize the image for rendering on ePaper displays""" | ||||
|  | ||||
|     buffer = numpy.array(image.convert('RGB')) | ||||
|     red, green = buffer[:, :, 0], buffer[:, :, 1] | ||||
|     # grey->black | ||||
|     buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] | ||||
|     image = Image.fromarray(buffer) | ||||
|     return image | ||||
|  | ||||
|   def calibrate(self): | ||||
|     """Calibrate the ePaper display to prevent burn-ins (ghosting) | ||||
|     use this command to manually calibrate the display""" | ||||
|  | ||||
|     self.Display.calibrate() | ||||
|  | ||||
|   def _calibration_check(self): | ||||
|     """Calibration sheduler | ||||
|     uses calibration hours from settings file to check if calibration is due""" | ||||
|     now = arrow.now() | ||||
|     print('hour:', now.hour, 'hours:', self._calibration_hours) | ||||
|     print('state:', self._calibration_state) | ||||
|     if now.hour in self._calibration_hours and self._calibration_state == False: | ||||
|       self.calibrate() | ||||
|       self._calibration_state = True | ||||
|     else: | ||||
|       self._calibration_state = False | ||||
|  | ||||
|  | ||||
|   def _check_for_updates(self): | ||||
|     """Check if a new update is available for inkycal""" | ||||
|  | ||||
|     raise NotImplementedError('Tha developer were too lazy to implement this..') | ||||
|  | ||||
|  | ||||
|   @staticmethod | ||||
|   def _add_module(filepath_module, classname): | ||||
|     """Add a third party module to inkycal | ||||
|     filepath_module = the full path of your module. The file should be in /modules! | ||||
|     classname = the name of your class inside the module | ||||
|     """ | ||||
|  | ||||
|     # Path for modules | ||||
|     _module_path = 'inkycal/modules/' | ||||
|  | ||||
|     # Check if the filepath is a string | ||||
|     if not isinstance(filepath_module, str): | ||||
|       raise ValueError('filepath has to be a string!') | ||||
|  | ||||
|     # Check if the classname is a string | ||||
|     if not isinstance(classname, str): | ||||
|       raise ValueError('classname has to be a string!') | ||||
|  | ||||
|     # TODO: | ||||
|     # Ensure only third-party modules are deleted as built-in modules | ||||
|     # should not be deleted | ||||
|  | ||||
|     # Check if module is inside the modules folder | ||||
|     if not _module_path in filepath_module: | ||||
|       raise Exception('Your module should be in', _module_path) | ||||
|  | ||||
|     # Get the name of the third-party module file without extension (.py) | ||||
|     filename = filepath_module.split('.py')[0].split('/')[-1] | ||||
|  | ||||
|     # Check if filename or classname is in the current module init file | ||||
|     with open('modules/__init__.py', mode ='r') as module_init: | ||||
|       content = module_init.read().splitlines() | ||||
|  | ||||
|     for line in content: | ||||
|       if (filename or clasname) in line: | ||||
|         raise Exception( | ||||
|           'A module with this filename or classname already exists') | ||||
|  | ||||
|     # Check if filename or classname is in the current inkycal init file | ||||
|     with open('__init__.py', mode ='r') as inkycal_init: | ||||
|       content = inkycal_init.read().splitlines() | ||||
|  | ||||
|     for line in content: | ||||
|       if (filename or clasname) in line: | ||||
|         raise Exception( | ||||
|           'A module with this filename or classname already exists') | ||||
|  | ||||
|     # If all checks have passed, add the module in the module init file | ||||
|     with open('modules/__init__.py', mode='a') as module_init: | ||||
|       module_init.write('from .{} import {}'.format(filename, classname)) | ||||
|  | ||||
|     # If all checks have passed, add the module in the inkycal init file | ||||
|     with open('__init__.py', mode ='a') as inkycal_init: | ||||
|       inkycal_init.write('# Added by module adder \n') | ||||
|       inkycal_init.write('import inkycal.modules.{}'.format(filename)) | ||||
|  | ||||
|     print('Your module {} has been added successfully! Hooray!'.format( | ||||
|       classname)) | ||||
|  | ||||
|   @staticmethod | ||||
|   def _remove_module(classname, remove_file = True): | ||||
|     """Removes a third-party module from inkycal | ||||
|     Input the classname of the file you want to remove  | ||||
|     """ | ||||
|  | ||||
|     # Check if filename or classname is in the current module init file | ||||
|     with open('modules/__init__.py', mode ='r') as module_init: | ||||
|       content = module_init.read().splitlines() | ||||
|  | ||||
|     with open('modules/__init__.py', mode ='w') as module_init: | ||||
|       for line in content: | ||||
|         if not classname in line: | ||||
|           module_init.write(line+'\n') | ||||
|         else: | ||||
|           filename = line.split(' ')[1].split('.')[1] | ||||
|  | ||||
|     # Check if filename or classname is in the current inkycal init file | ||||
|     with open('__init__.py', mode ='r') as inkycal_init: | ||||
|       content = inkycal_init.read().splitlines() | ||||
|  | ||||
|     with open('__init__.py', mode ='w') as inkycal_init: | ||||
|       for line in content: | ||||
|         if not filename in line: | ||||
|           inkycal_init.write(line+'\n') | ||||
|  | ||||
|     # remove the file of the third party module if it exists and remove_file | ||||
|     # was set to True (default) | ||||
|     if os.path.exists('modules/{}.py'.format(filename)) and remove_file == True: | ||||
|       os.remove('modules/{}.py'.format(filename)) | ||||
|  | ||||
|     print('The module {} has been removed successfully'.format(classname)) | ||||
|  | ||||
|   | ||||
| @@ -212,3 +212,10 @@ class iCalendar: | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone mode'.format(filename)) | ||||
|  | ||||
|   a = iCalendar() | ||||
|   now = arrow.now() | ||||
|   a.load_url('https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics') | ||||
|   a.load_url('https://calendar.yahoo.com/saadnaseer63/37435f792ecb221cdd169d06a518b30f/ycal.ics?id=1670') | ||||
|   a.get_events(now, now.shift(weeks=2), a.get_system_tz()) | ||||
|   a.show_events() | ||||
|   | ||||
| @@ -16,35 +16,60 @@ filename = os.path.basename(__file__).split('.py')[0] | ||||
| logger = logging.getLogger(filename) | ||||
| logger.setLevel(level=logging.ERROR) | ||||
|  | ||||
|  | ||||
| class Agenda(inkycal_module): | ||||
|   """Agenda class | ||||
|   Create agenda and show events from given icalendars | ||||
|   """ | ||||
|  | ||||
|   name = "Inkycal Agenda" | ||||
|  | ||||
|   requires = { | ||||
|     "ical_urls" : { | ||||
|       "label":"iCalendar URL/s, separate multiple ones with a comma", | ||||
|       }, | ||||
|  | ||||
|     } | ||||
|  | ||||
|   optional = { | ||||
|     "ical_files" : { | ||||
|       "label":"iCalendar filepaths, separated with a comma", | ||||
|       "default":[] | ||||
|       }, | ||||
|  | ||||
|     "date_format":{ | ||||
|       "label":"Use an arrow-supported token for custom date formatting "+ | ||||
|       "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. ddd D MMM", | ||||
|       "default": "ddd D MMM", | ||||
|       }, | ||||
|  | ||||
|     "time_format":{ | ||||
|       "label":"Use an arrow-supported token for custom time formatting "+ | ||||
|       "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", | ||||
|       }, | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_agenda module""" | ||||
|  | ||||
|     super().__init__(section_size, section_config) | ||||
|     # Module specific parameters | ||||
|     required = ['week_starts_on', 'ical_urls'] | ||||
|     for param in required: | ||||
|  | ||||
|     for param in self.equires: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|     # class name | ||||
|     self.name = self.__class__.__name__ | ||||
|  | ||||
|     # module specific parameters | ||||
|     self.date_format = 'ddd D MMM' | ||||
|     self.time_format = "HH:mm" | ||||
|     self.date_format = self.config['date_format'] | ||||
|     self.time_format = self.config['time_format'] | ||||
|     self.language = self.config['language'] | ||||
|     self.timezone = get_system_tz() | ||||
|     self.ical_urls = self.config['ical_urls'] | ||||
|     self.ical_files = [] | ||||
|     self.ical_files = self.config['ical_files'] | ||||
|  | ||||
|     self.timezone = get_system_tz() | ||||
|  | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|     print('{0} loaded'.format(filename)) | ||||
|  | ||||
|   def _validate(self): | ||||
|     """Validate module-specific parameters""" | ||||
| @@ -191,7 +216,6 @@ class Agenda(inkycal_module): | ||||
|  | ||||
|     # If no events were found, write only dates and lines | ||||
|     else: | ||||
|       line_pos = [(0, int(line * line_height)) for line in range(max_lines)] | ||||
|       cursor = 0 | ||||
|       for _ in agenda_events: | ||||
|         title = _['title'] | ||||
| @@ -206,9 +230,8 @@ class Agenda(inkycal_module): | ||||
|  | ||||
|       logger.info('no events found') | ||||
|  | ||||
|     # Save image of black and colour channel in image-folder | ||||
|     im_black.save(images+self.name+'.png') | ||||
|     im_colour.save(images+self.name+'_colour.png') | ||||
|     # return the images ready for the display | ||||
|     return im_black, im_colour | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone mode'.format(filename)) | ||||
|   | ||||
| @@ -19,42 +19,74 @@ class Calendar(inkycal_module): | ||||
|   Create monthly calendar and show events from given icalendars | ||||
|   """ | ||||
|  | ||||
|   name = "Inkycal Calendar" | ||||
|  | ||||
|   optional = { | ||||
|      | ||||
|     "week_starts_on" : { | ||||
|       "label":"When does your week start? (default=Monday)", | ||||
|       "options": ["Monday", "Sunday"], | ||||
|       "default": "Monday" | ||||
|       }, | ||||
|  | ||||
|     "show_events" : { | ||||
|       "label":"Show parsed events? (default = True)", | ||||
|       "options": [True, False], | ||||
|       "default": True | ||||
|       }, | ||||
|  | ||||
|     "ical_urls" : { | ||||
|       "label":"iCalendar URL/s, separate multiple ones with a comma", | ||||
|       "default":[] | ||||
|       }, | ||||
|  | ||||
|     "ical_files" : { | ||||
|       "label":"iCalendar filepaths, separated with a comma", | ||||
|       "default":[] | ||||
|       }, | ||||
|      | ||||
|     "date_format":{ | ||||
|       "label":"Use an arrow-supported token for custom date formatting "+ | ||||
|       "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", | ||||
|       "default": "D MMM", | ||||
|       }, | ||||
|  | ||||
|     "time_format":{ | ||||
|       "label":"Use an arrow-supported token for custom time formatting "+ | ||||
|       "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", | ||||
|       "default": "HH:mm" | ||||
|       }, | ||||
|      | ||||
|     } | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_calendar module""" | ||||
|  | ||||
|     super().__init__(section_size, section_config) | ||||
|  | ||||
|     # Module specific parameters | ||||
|     required = ['week_starts_on'] | ||||
|     for param in required: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|     # module name | ||||
|     self.name = self.__class__.__name__ | ||||
|  | ||||
|     # module specific parameters | ||||
|     self.num_font = ImageFont.truetype( | ||||
|       fonts['NotoSans-SemiCondensed'], size = self.fontsize) | ||||
|     self.weekstart = self.config['week_starts_on'] | ||||
|     self.show_events = True | ||||
|     self.date_format = 'D MMM' | ||||
|     self.time_format = "HH:mm" | ||||
|     self.show_events = self.config['show_events'] | ||||
|     self.date_format = self.config["date_format"] | ||||
|     self.time_format = self.config['time_format'] | ||||
|     self.language = self.config['language'] | ||||
|  | ||||
|     self.timezone = get_system_tz() | ||||
|     self.ical_urls = self.config['ical_urls'] | ||||
|     self.ical_files = [] | ||||
|     self.ical_files = self.config['ical_files'] | ||||
|  | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|     print('{0} loaded'.format(filename)) | ||||
|  | ||||
|   def generate_image(self): | ||||
|     """Generate image for this module""" | ||||
|  | ||||
|     # Define new image size with respect to padding | ||||
|     im_width = int(self.width - (self.width * 2 * self.margin_x)) | ||||
|     im_height = int(self.height - (self.height * 2 * self.margin_y)) | ||||
|     im_width = int(self.width - (2 * self.padding_x)) | ||||
|     im_height = int(self.height - (2 * self.padding_y)) | ||||
|     im_size = im_width, im_height | ||||
|  | ||||
|     logger.info('Image size: {0}'.format(im_size)) | ||||
| @@ -80,15 +112,7 @@ class Calendar(inkycal_module): | ||||
|         im_width, calendar_height)) | ||||
|  | ||||
|     # Create grid and calculate icon sizes | ||||
|     now = arrow.now(tz = self.timezone) | ||||
|     monthstart = now.span('month')[0].weekday() | ||||
|     monthdays = now.ceil('month').day | ||||
|  | ||||
|     if monthstart > 4 and monthdays == 31: | ||||
|         calendar_rows, calendar_cols = 7, 7 | ||||
|     else: | ||||
|     calendar_rows, calendar_cols = 6, 7 | ||||
|  | ||||
|     icon_width = im_width // calendar_cols | ||||
|     icon_height = calendar_height // calendar_rows | ||||
|  | ||||
| @@ -106,6 +130,8 @@ class Calendar(inkycal_module): | ||||
|     weekday_pos = [(grid_start_x + icon_width*_, month_name_height) for _ in | ||||
|                    range(calendar_cols)] | ||||
|  | ||||
|     now = arrow.now(tz = self.timezone) | ||||
|  | ||||
|     # Set weekstart of calendar to specified weekstart | ||||
|     if self.weekstart == "Monday": | ||||
|       cal.setfirstweekday(cal.MONDAY) | ||||
| @@ -283,9 +309,8 @@ class Calendar(inkycal_module): | ||||
|               (im_width, self.font.getsize(symbol)[1]), symbol, | ||||
|               font = self.font) | ||||
|  | ||||
|     # Save image of black and colour channel in image-folder | ||||
|     im_black.save(images+self.name+'.png') | ||||
|     im_colour.save(images+self.name+'_colour.png') | ||||
|     # return the images ready for the display | ||||
|     return im_black, im_colour | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone mode'.format(filename)) | ||||
|   | ||||
| @@ -1,305 +1,32 @@ | ||||
| #!/usr/bin/python3 | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| Image module for Inkycal Project | ||||
| Image module for inkycal Project | ||||
| Copyright by aceisace | ||||
| Development satge: Beta | ||||
| """ | ||||
|  | ||||
| from inkycal.modules.template import inkycal_module | ||||
| from inkycal.custom import * | ||||
|  | ||||
| from os import path | ||||
| from PIL import ImageOps | ||||
| import requests | ||||
| import numpy | ||||
|  | ||||
| filename = os.path.basename(__file__).split('.py')[0] | ||||
| logger = logging.getLogger(filename) | ||||
| logger.setLevel(level=logging.DEBUG) | ||||
| """----------------------------------------------------------------""" | ||||
| #path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' | ||||
| #path  ='/home/pi/Inky-Calendar/images/canvas.png' | ||||
| path      = inkycal_image_path | ||||
| path_body = inkycal_image_path_body | ||||
| mode = 'auto'         # 'horizontal' # 'vertical' # 'auto' | ||||
| upside_down = False    # Flip image by 180 deg (upside-down) | ||||
| alignment = 'center'  # top_center, top_left, center_left, bottom_right etc. | ||||
| colours = 'bwr'       # bwr # bwy # bw | ||||
| render = True         # show image on E-Paper? | ||||
| """----------------------------------------------------------------""" | ||||
|  | ||||
| class Inkyimage(inkycal_module): | ||||
|   """Image class | ||||
|   display an image from a given path or URL | ||||
|   """ | ||||
|   _allowed_layout = ['fill', 'center', 'fit', 'auto'] | ||||
|   _allowed_rotation = [0, 90, 180, 270, 360, 'auto'] | ||||
|   _allowed_colours = ['bw', 'bwr', 'bwy'] | ||||
| # First determine dimensions | ||||
| if mode == 'horizontal': | ||||
|   display_width, display_height == display_height, display_width | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_rss module""" | ||||
|  | ||||
|     super().__init__(section_size, section_config) | ||||
|  | ||||
|     # Module specific parameters | ||||
|     required = ['path'] | ||||
|     for param in required: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|     # module name | ||||
|     self.name = self.__class__.__name__ | ||||
|  | ||||
|     # module specific parameters | ||||
|     self.image_path = self.config['path'] | ||||
|  | ||||
|     self.rotation = 0 #0 #90 # 180 # 270 # auto | ||||
|     self.layout = 'fill' # centre # fit # auto | ||||
|     self.colours = 'bw' #grab from settings file? | ||||
|  | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|  | ||||
|   def _validate(self): | ||||
|     """Validate module-specific parameters""" | ||||
|  | ||||
|     # Validate image_path | ||||
|     if not isinstance(self.image_path, str): | ||||
|       print( | ||||
|         'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"') | ||||
|  | ||||
|     # Validate layout | ||||
|     if not isinstance(self.layout, str) or ( | ||||
|       self.layout not in self._allowed_layout): | ||||
|       print('layout has to be one of the following:', self._allowed_layout) | ||||
|  | ||||
|     # Validate rotation angle | ||||
|     if self.rotation not in self._allowed_rotation: | ||||
|       print('rotation has to be one of the following:', self._allowed_rotation) | ||||
|  | ||||
|     # Validate colours | ||||
|     if not isinstance(self.colours, str) or ( | ||||
|       self.colours not in self._allowed_colours): | ||||
|       print('colour has to be one of the following:', self._allowed_colours) | ||||
|  | ||||
|   def generate_image(self): | ||||
|     """Generate image for this module""" | ||||
|  | ||||
|     # Define new image size with respect to padding | ||||
|     im_width = self.width | ||||
|     im_height = self.height | ||||
|     im_size = im_width, im_height | ||||
|     logger.info('image size: {} x {} px'.format(im_width, im_height)) | ||||
|  | ||||
|     # Try to open the image if it exists and is an image file | ||||
|     try: | ||||
|       if self.image_path.startswith('http'): | ||||
|         logger.debug('identified url') | ||||
|         self.image = Image.open(requests.get(self.image_path, stream=True).raw) | ||||
|       else: | ||||
|         logger.info('identified local path') | ||||
|         self.image = Image.open(self.image_path) | ||||
|     except FileNotFoundError: | ||||
|       raise ('Your file could not be found. Please check the filepath') | ||||
|     except OSError: | ||||
|       raise ('Please check if the path points to an image file.') | ||||
|  | ||||
|     logger.debug(('image-width:', self.image.width)) | ||||
|     logger.debug(('image-height:', self.image.height)) | ||||
|  | ||||
|     # Create an image for black pixels and one for coloured pixels | ||||
|     im_black = Image.new('RGB', size = im_size, color = 'white') | ||||
|     im_colour = Image.new('RGB', size = im_size, color = 'white') | ||||
|  | ||||
|     # do the required operations | ||||
|     self._remove_alpha() | ||||
|     self._to_layout() | ||||
|     black, colour = self._map_colours() | ||||
|  | ||||
|     # paste the imaeges on the canvas | ||||
|     im_black.paste(black, (self.x, self.y)) | ||||
|     if colour != None: | ||||
|       im_colour.paste(colour, (self.x, self.y)) | ||||
|  | ||||
|     # Save image of black and colour channel in image-folder | ||||
|     im_black.save(images+self.name+'.png', 'PNG') | ||||
|     if colour != None: | ||||
|       im_colour.save(images+self.name+'_colour.png', 'PNG') | ||||
|  | ||||
|   def _rotate(self, angle=None): | ||||
|     """Rotate the image to a given angle | ||||
|     angle must be one of :[0, 90, 180, 270, 360, 'auto'] | ||||
|     """ | ||||
|     im = self.image | ||||
|     if angle == None: | ||||
|       angle = self.rotation | ||||
|  | ||||
|     # Check if angle is supported | ||||
|     if angle not in self._allowed_rotation: | ||||
|       print('invalid angle provided, setting to fallback: 0 deg') | ||||
|       angle = 0 | ||||
|  | ||||
|     # Autoflip the image if angle == 'auto' | ||||
|     if angle == 'auto': | ||||
|       if (im.width > self.height) and (im.width < self.height): | ||||
|         print('display vertical, image horizontal -> flipping image') | ||||
|         image = im.rotate(90, expand=True) | ||||
|       if (im.width < self.height) and (im.width > self.height): | ||||
|         print('display horizontal, image vertical -> flipping image') | ||||
|         image = im.rotate(90, expand=True) | ||||
|     # if not auto, flip to specified angle | ||||
|     else: | ||||
|       image = im.rotate(angle, expand = True) | ||||
|     self.image = image | ||||
|  | ||||
|   def _fit_width(self, width=None): | ||||
|     """Resize an image to desired width""" | ||||
|     im = self.image | ||||
|     if width == None: width = self.width | ||||
|  | ||||
|     logger.debug(('resizing width from', im.width, 'to')) | ||||
|     wpercent = (width/float(im.width)) | ||||
|     hsize = int((float(im.height)*float(wpercent))) | ||||
|     image = im.resize((width, hsize), Image.ANTIALIAS) | ||||
|     logger.debug(image.width) | ||||
|     self.image = image | ||||
|  | ||||
|   def _fit_height(self, height=None): | ||||
|     """Resize an image to desired height""" | ||||
|     im = self.image | ||||
|     if height == None: height = self.height | ||||
|  | ||||
|     logger.debug(('resizing height from', im.height, 'to')) | ||||
|     hpercent = (height / float(im.height)) | ||||
|     wsize = int(float(im.width) * float(hpercent)) | ||||
|     image = im.resize((wsize, height), Image.ANTIALIAS) | ||||
|     logger.debug(image.height) | ||||
|     self.image = image | ||||
|  | ||||
|   def _to_layout(self, mode=None): | ||||
|     """Adjust the image to suit the layout | ||||
|     mode can be center, fit or fill""" | ||||
|  | ||||
|     im = self.image | ||||
|     if mode == None: mode = self.layout | ||||
|  | ||||
|     if mode not in self._allowed_layout: | ||||
|       print('{} is not supported. Should be one of {}'.format( | ||||
|         mode, self._allowed_layout)) | ||||
|       print('setting layout to fallback: centre') | ||||
|       mode = 'center' | ||||
|  | ||||
|     # If mode is center, just center the image | ||||
|     if mode == 'center': | ||||
|       pass | ||||
|  | ||||
|     # if mode is fit, adjust height of the image while keeping ascept-ratio | ||||
|     if mode == 'fit': | ||||
|       self._fit_height() | ||||
|  | ||||
|     # if mode is fill, enlargen or shrink the image to fit width | ||||
|     if mode == 'fill': | ||||
|       self._fit_width() | ||||
|  | ||||
|     # in auto mode, flip image automatically and fit both height and width | ||||
|     if mode == 'auto': | ||||
|  | ||||
|       # Check if width is bigger than height and rotate by 90 deg if true | ||||
|       if im.width > im.height: | ||||
|         self._rotate(90) | ||||
|  | ||||
|       # fit both height and width | ||||
|       self._fit_height() | ||||
|       self._fit_width() | ||||
|  | ||||
|     if self.image.width > self.width: | ||||
|       x = int( (self.image.width - self.width) / 2) | ||||
|     else: | ||||
|       x = int( (self.width - self.image.width) / 2) | ||||
|  | ||||
|     if self.image.height > self.height: | ||||
|       y = int( (self.image.height - self.height) / 2) | ||||
|     else: | ||||
|       y = int( (self.height - self.image.height) / 2) | ||||
|  | ||||
|     self.x, self.y = x, y | ||||
|  | ||||
|   def _remove_alpha(self): | ||||
|     im = self.image | ||||
|  | ||||
|     if len(im.getbands()) == 4: | ||||
|       logger.debug('removing transparency') | ||||
|       bg = Image.new('RGBA', (im.width, im.height), 'white') | ||||
|       im = Image.alpha_composite(bg, im) | ||||
|     self.image.paste(im, (0,0)) | ||||
|  | ||||
|   def _map_colours(self, colours = None): | ||||
|     """Map image colours to display-supported colours """ | ||||
|     im = self.image.convert('RGB') | ||||
|     if colours == None: colours = self.colours | ||||
|  | ||||
|     if colours not in self._allowed_colours: | ||||
|       print('invalid colour: "{}", has to be one of: {}'.format( | ||||
|         colours, self._allowed_colours)) | ||||
|       print('setting to fallback: bw') | ||||
|       colours = 'bw' | ||||
|  | ||||
|     if colours == 'bw': | ||||
|  | ||||
|       # For black-white images, use monochrome dithering | ||||
|       im_black = im.convert('1', dither=True) | ||||
|       im_colour = None | ||||
|  | ||||
|     elif colours == 'bwr': | ||||
|       # For black-white-red images, create corresponding palette | ||||
|       pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] | ||||
|  | ||||
|     elif colours == 'bwy': | ||||
|       # For black-white-yellow images, create corresponding palette""" | ||||
|       pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255] | ||||
|  | ||||
|     # Map each pixel of the opened image to the Palette | ||||
|     if colours == 'bwr' or colours == 'bwy': | ||||
|       palette_im = Image.new('P', (3,1)) | ||||
|       palette_im.putpalette(pal * 64) | ||||
|       quantized_im = im.quantize(palette=palette_im) | ||||
|       quantized_im.convert('RGB') | ||||
|  | ||||
|       # Create buffer for coloured pixels | ||||
|       buffer1 = numpy.array(quantized_im.convert('RGB')) | ||||
|       r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] | ||||
|  | ||||
|       # Create buffer for black pixels | ||||
|       buffer2 = numpy.array(quantized_im.convert('RGB')) | ||||
|       r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] | ||||
|  | ||||
|       if colours == 'bwr': | ||||
|         # Create image for only red pixels | ||||
|         buffer2[numpy.logical_and(r2 ==  0, b2 == 0)] = [255,255,255] # black->white | ||||
|         buffer2[numpy.logical_and(r2 ==  255, b2 == 0)] = [0,0,0] #red->black | ||||
|         im_colour = Image.fromarray(buffer2) | ||||
|  | ||||
|         # Create image for only black pixels | ||||
|         buffer1[numpy.logical_and(r1 ==  255, b1 == 0)] = [255,255,255] | ||||
|         im_black = Image.fromarray(buffer1) | ||||
|  | ||||
|       if colours == 'bwy': | ||||
|         # Create image for only yellow pixels | ||||
|         buffer2[numpy.logical_and(r2 ==  0, b2 == 0)] = [255,255,255] # black->white | ||||
|         buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black | ||||
|         im_colour = Image.fromarray(buffer2) | ||||
|  | ||||
|         # Create image for only black pixels | ||||
|         buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] | ||||
|         im_black = Image.fromarray(buffer1) | ||||
|  | ||||
|     return im_black, im_colour | ||||
|  | ||||
|   @staticmethod | ||||
|   def save(image): | ||||
|     im = self.image | ||||
|     im.save('/home/pi/Desktop/test.png', 'PNG') | ||||
|  | ||||
|   @staticmethod | ||||
|   def _show(image): | ||||
|     """Preview the image on gpicview (only works on Rapsbian with Desktop)""" | ||||
|     path = '/home/pi/Desktop/' | ||||
|     image.save(path+'temp.png') | ||||
|     os.system("gpicview "+path+'temp.png') | ||||
|     os.system('rm '+path+'temp.png') | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone/debug mode'.format(filename)) | ||||
|  | ||||
| ##  a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) | ||||
| ##  a.generate_image() | ||||
|      | ||||
| print('Done') | ||||
|   | ||||
							
								
								
									
										305
									
								
								inkycal/modules/inkycal_image2.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										305
									
								
								inkycal/modules/inkycal_image2.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,305 @@ | ||||
| #!/usr/bin/python3 | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| Image module for Inkycal Project | ||||
| Copyright by aceisace | ||||
| """ | ||||
|  | ||||
| from inkycal.modules.template import inkycal_module | ||||
| from inkycal.custom import * | ||||
|  | ||||
| from PIL import ImageOps | ||||
| import requests | ||||
| import numpy | ||||
|  | ||||
| filename = os.path.basename(__file__).split('.py')[0] | ||||
| logger = logging.getLogger(filename) | ||||
| logger.setLevel(level=logging.ERROR) | ||||
|  | ||||
| class Inkyimage(inkycal_module): | ||||
|   """Image class | ||||
|   display an image from a given path or URL | ||||
|   """ | ||||
|  | ||||
|   name = "Inykcal Image - show an image from a URL or local path" | ||||
|    | ||||
|   requires = { | ||||
|   'path': { | ||||
|     "label":"Please enter the path of the image file (local or URL)", | ||||
|     } | ||||
|  | ||||
|   } | ||||
|  | ||||
|   optional = { | ||||
|   'rotation':{ | ||||
|     "label":"Specify the angle to rotate the image. Default is 0", | ||||
|     "options": [0, 90, 180, 270, 360, "auto"], | ||||
|     "default":0, | ||||
|   }, | ||||
|   'layout':{ | ||||
|     "label":"How should the image be displayed on the display? Default is auto", | ||||
|     "options": ['fill', 'center', 'fit', 'auto'], | ||||
|     "default": "auto" | ||||
|   } | ||||
|  | ||||
|   } | ||||
|  | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_rss module""" | ||||
|  | ||||
|     super().__init__(section_size, section_config) | ||||
|  | ||||
|     # required parameters | ||||
|     for param in self.requires: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|     # optional parameters | ||||
|     self.image_path = self.config['path'] | ||||
|  | ||||
|     self.rotation = self.config['rotation'] | ||||
|     self.layout = self.config['layout'] | ||||
|  | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|  | ||||
|   def _validate(self): | ||||
|     """Validate module-specific parameters""" | ||||
|  | ||||
|     # Validate image_path | ||||
|     if not isinstance(self.image_path, str): | ||||
|       print( | ||||
|         'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"') | ||||
|  | ||||
|     # Validate layout | ||||
|     if not isinstance(self.layout, str): | ||||
|       print('layout has to be a string') | ||||
|  | ||||
|   def generate_image(self): | ||||
|     """Generate image for this module""" | ||||
|  | ||||
|     # Define new image size with respect to padding | ||||
|     im_width = self.width | ||||
|     im_height = self.height | ||||
|     im_size = im_width, im_height | ||||
|     logger.info('image size: {} x {} px'.format(im_width, im_height)) | ||||
|  | ||||
|     # Try to open the image if it exists and is an image file | ||||
|     try: | ||||
|       if self.image_path.startswith('http'): | ||||
|         logger.debug('identified url') | ||||
|         self.image = Image.open(requests.get(self.image_path, stream=True).raw) | ||||
|       else: | ||||
|         logger.info('identified local path') | ||||
|         self.image = Image.open(self.image_path) | ||||
|     except FileNotFoundError: | ||||
|       raise ('Your file could not be found. Please check the filepath') | ||||
|     except OSError: | ||||
|       raise ('Please check if the path points to an image file.') | ||||
|  | ||||
|     logger.debug(('image-width:', self.image.width)) | ||||
|     logger.debug(('image-height:', self.image.height)) | ||||
|  | ||||
|     # Create an image for black pixels and one for coloured pixels | ||||
|     im_black = Image.new('RGB', size = im_size, color = 'white') | ||||
|     im_colour = Image.new('RGB', size = im_size, color = 'white') | ||||
|  | ||||
|     # do the required operations | ||||
|     self._remove_alpha() | ||||
|     self._to_layout() | ||||
|     black, colour = self._map_colours() | ||||
|  | ||||
|     # paste the images on the canvas | ||||
|     im_black.paste(black, (self.x, self.y)) | ||||
|     im_colour.paste(colour, (self.x, self.y)) | ||||
|  | ||||
|     # Save images of black and colour channel in image-folder | ||||
|     im_black.save(images+self.name+'.png', 'PNG') | ||||
|     im_colour.save(images+self.name+'_colour.png', 'PNG') | ||||
|  | ||||
|   def _rotate(self, angle=None): | ||||
|     """Rotate the image to a given angle | ||||
|     angle must be one of :[0, 90, 180, 270, 360, 'auto'] | ||||
|     """ | ||||
|     im = self.image | ||||
|     if angle == None: | ||||
|       angle = self.rotation | ||||
|  | ||||
|     # Check if angle is supported | ||||
|     if angle not in self._allowed_rotation: | ||||
|       print('invalid angle provided, setting to fallback: 0 deg') | ||||
|       angle = 0 | ||||
|  | ||||
|     # Autoflip the image if angle == 'auto' | ||||
|     if angle == 'auto': | ||||
|       if (im.width > self.height) and (im.width < self.height): | ||||
|         print('display vertical, image horizontal -> flipping image') | ||||
|         image = im.rotate(90, expand=True) | ||||
|       if (im.width < self.height) and (im.width > self.height): | ||||
|         print('display horizontal, image vertical -> flipping image') | ||||
|         image = im.rotate(90, expand=True) | ||||
|     # if not auto, flip to specified angle | ||||
|     else: | ||||
|       image = im.rotate(angle, expand = True) | ||||
|     self.image = image | ||||
|  | ||||
|   def _fit_width(self, width=None): | ||||
|     """Resize an image to desired width""" | ||||
|     im = self.image | ||||
|     if width == None: width = self.width | ||||
|  | ||||
|     logger.debug(('resizing width from', im.width, 'to')) | ||||
|     wpercent = (width/float(im.width)) | ||||
|     hsize = int((float(im.height)*float(wpercent))) | ||||
|     image = im.resize((width, hsize), Image.ANTIALIAS) | ||||
|     logger.debug(image.width) | ||||
|     self.image = image | ||||
|  | ||||
|   def _fit_height(self, height=None): | ||||
|     """Resize an image to desired height""" | ||||
|     im = self.image | ||||
|     if height == None: height = self.height | ||||
|  | ||||
|     logger.debug(('resizing height from', im.height, 'to')) | ||||
|     hpercent = (height / float(im.height)) | ||||
|     wsize = int(float(im.width) * float(hpercent)) | ||||
|     image = im.resize((wsize, height), Image.ANTIALIAS) | ||||
|     logger.debug(image.height) | ||||
|     self.image = image | ||||
|  | ||||
|   def _to_layout(self, mode=None): | ||||
|     """Adjust the image to suit the layout | ||||
|     mode can be center, fit or fill""" | ||||
|  | ||||
|     im = self.image | ||||
|     if mode == None: mode = self.layout | ||||
|  | ||||
|     if mode not in self._allowed_layout: | ||||
|       print('{} is not supported. Should be one of {}'.format( | ||||
|         mode, self._allowed_layout)) | ||||
|       print('setting layout to fallback: centre') | ||||
|       mode = 'center' | ||||
|  | ||||
|     # If mode is center, just center the image | ||||
|     if mode == 'center': | ||||
|       pass | ||||
|  | ||||
|     # if mode is fit, adjust height of the image while keeping ascept-ratio | ||||
|     if mode == 'fit': | ||||
|       self._fit_height() | ||||
|  | ||||
|     # if mode is fill, enlargen or shrink the image to fit width | ||||
|     if mode == 'fill': | ||||
|       self._fit_width() | ||||
|  | ||||
|     # in auto mode, flip image automatically and fit both height and width | ||||
|     if mode == 'auto': | ||||
|  | ||||
|       # Check if width is bigger than height and rotate by 90 deg if true | ||||
|       if im.width > im.height: | ||||
|         self._rotate(90) | ||||
|  | ||||
|       # fit both height and width | ||||
|       self._fit_height() | ||||
|       self._fit_width() | ||||
|  | ||||
|     if self.image.width > self.width: | ||||
|       x = int( (self.image.width - self.width) / 2) | ||||
|     else: | ||||
|       x = int( (self.width - self.image.width) / 2) | ||||
|  | ||||
|     if self.image.height > self.height: | ||||
|       y = int( (self.image.height - self.height) / 2) | ||||
|     else: | ||||
|       y = int( (self.height - self.image.height) / 2) | ||||
|  | ||||
|     self.x, self.y = x, y | ||||
|  | ||||
|   def _remove_alpha(self): | ||||
|     im = self.image | ||||
|  | ||||
|     if len(im.getbands()) == 4: | ||||
|       logger.debug('removing transparency') | ||||
|       bg = Image.new('RGBA', (im.width, im.height), 'white') | ||||
|       im = Image.alpha_composite(bg, im) | ||||
|     self.image.paste(im, (0,0)) | ||||
|  | ||||
|   def _map_colours(self, colours = None): | ||||
|     """Map image colours to display-supported colours """ | ||||
|     im = self.image.convert('RGB') | ||||
|  | ||||
|     if colours == 'bw': | ||||
|  | ||||
|       # For black-white images, use monochrome dithering | ||||
|       im_black = im.convert('1', dither=True) | ||||
|       im_colour = None | ||||
|  | ||||
|     elif colours == 'bwr': | ||||
|       # For black-white-red images, create corresponding palette | ||||
|       pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] | ||||
|  | ||||
|     elif colours == 'bwy': | ||||
|       # For black-white-yellow images, create corresponding palette""" | ||||
|       pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255] | ||||
|  | ||||
|     # Map each pixel of the opened image to the Palette | ||||
|     if colours == 'bwr' or colours == 'bwy': | ||||
|       palette_im = Image.new('P', (3,1)) | ||||
|       palette_im.putpalette(pal * 64) | ||||
|       quantized_im = im.quantize(palette=palette_im) | ||||
|       quantized_im.convert('RGB') | ||||
|  | ||||
|       # Create buffer for coloured pixels | ||||
|       buffer1 = numpy.array(quantized_im.convert('RGB')) | ||||
|       r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] | ||||
|  | ||||
|       # Create buffer for black pixels | ||||
|       buffer2 = numpy.array(quantized_im.convert('RGB')) | ||||
|       r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] | ||||
|  | ||||
|       if colours == 'bwr': | ||||
|         # Create image for only red pixels | ||||
|         buffer2[numpy.logical_and(r2 ==  0, b2 == 0)] = [255,255,255] # black->white | ||||
|         buffer2[numpy.logical_and(r2 ==  255, b2 == 0)] = [0,0,0] #red->black | ||||
|         im_colour = Image.fromarray(buffer2) | ||||
|  | ||||
|         # Create image for only black pixels | ||||
|         buffer1[numpy.logical_and(r1 ==  255, b1 == 0)] = [255,255,255] | ||||
|         im_black = Image.fromarray(buffer1) | ||||
|  | ||||
|       if colours == 'bwy': | ||||
|         # Create image for only yellow pixels | ||||
|         buffer2[numpy.logical_and(r2 ==  0, b2 == 0)] = [255,255,255] # black->white | ||||
|         buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black | ||||
|         im_colour = Image.fromarray(buffer2) | ||||
|  | ||||
|         # Create image for only black pixels | ||||
|         buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] | ||||
|         im_black = Image.fromarray(buffer1) | ||||
|  | ||||
|     return im_black, im_colour | ||||
|  | ||||
|   @staticmethod | ||||
|   def save(image, path): | ||||
|     im = self.image | ||||
|     im.save(path, 'PNG') | ||||
|  | ||||
|   @staticmethod | ||||
|   def _show(image): | ||||
|     """Preview the image on gpicview (only works on Rapsbian with Desktop)""" | ||||
|     path = '/home/pi/Desktop/' | ||||
|     image.save(path+'temp.png') | ||||
|     os.system("gpicview "+path+'temp.png') | ||||
|     os.system('rm '+path+'temp.png') | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone/debug mode'.format(filename)) | ||||
|  | ||||
|   #a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) | ||||
|   #a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) | ||||
|   a = Inkyimage((480, 800), {'path': "/home/pi/Desktop/im/IMG_0475.JPG"}) | ||||
|   a.generate_image() | ||||
|    | ||||
|  | ||||
| @@ -2,7 +2,7 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| RSS module for Inky-Calendar Project | ||||
| RSS module for inkyCal Project | ||||
| Copyright by aceisace | ||||
| """ | ||||
|  | ||||
| @@ -25,28 +25,48 @@ class RSS(inkycal_module): | ||||
|   parses rss/atom feeds from given urls | ||||
|   """ | ||||
|  | ||||
|   name = "Inkycal RSS / Atom" | ||||
|  | ||||
|   requires = { | ||||
|     "rss_urls" : { | ||||
|       "label":"Please enter ATOM or RSS feed URL/s, separated by a comma", | ||||
|       }, | ||||
|  | ||||
|     } | ||||
|  | ||||
|   optional = { | ||||
|      | ||||
|     "shuffle_feeds": { | ||||
|       "label": "Should the parsed RSS feeds be shuffled? (default=True)", | ||||
|       "options": [True, False], | ||||
|       "default": True | ||||
|       }, | ||||
|  | ||||
|     } | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_rss module""" | ||||
|  | ||||
|     super().__init__(section_size, section_config) | ||||
|  | ||||
|     # Module specific parameters | ||||
|     required = ['rss_urls'] | ||||
|     for param in required: | ||||
|     # Check if required parameters are available in config | ||||
|     for param in self.requires: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|     # module name | ||||
|     self.name = self.__class__.__name__ | ||||
|     # parse required config | ||||
|     self.rss_urls = self.config["rss_urls"].split(",") | ||||
|  | ||||
|     # parse optional config | ||||
|     self.shuffle_feeds = self.config["shuffle_feeds"] | ||||
|                     | ||||
|     # module specific parameters | ||||
|     self.shuffle_feeds = True | ||||
|  | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|     print('{0} loaded'.format(filename)) | ||||
|  | ||||
|   def _validate(self): | ||||
|     """Validate module-specific parameters""" | ||||
|  | ||||
|     if not isinstance(self.shuffle_feeds, bool): | ||||
|       print('shuffle_feeds has to be a boolean: True/False') | ||||
|  | ||||
| @@ -55,8 +75,8 @@ class RSS(inkycal_module): | ||||
|     """Generate image for this module""" | ||||
|  | ||||
|     # Define new image size with respect to padding | ||||
|     im_width = int(self.width - (self.width * 2 * self.margin_x)) | ||||
|     im_height = int(self.height - (self.height * 2 * self.margin_y)) | ||||
|     im_width = int(self.width - ( 2 * self.padding_x)) | ||||
|     im_height = int(self.height - (2 * self.padding_y)) | ||||
|     im_size = im_width, im_height | ||||
|     logger.info('image size: {} x {} px'.format(im_width, im_height)) | ||||
|  | ||||
| @@ -70,7 +90,6 @@ class RSS(inkycal_module): | ||||
|     else: | ||||
|       raise Exception('Network could not be reached :/') | ||||
|  | ||||
|  | ||||
|     # Set some parameters for formatting rss feeds | ||||
|     line_spacing = 1 | ||||
|     line_height = self.font.getsize('hg')[1] + line_spacing | ||||
| @@ -86,7 +105,7 @@ class RSS(inkycal_module): | ||||
|  | ||||
|     # Create list containing all rss-feeds from all rss-feed urls | ||||
|     parsed_feeds = [] | ||||
|     for feeds in self.config['rss_urls']: | ||||
|     for feeds in self.rss_urls: | ||||
|       text = feedparser.parse(feeds) | ||||
|       for posts in text.entries: | ||||
|         parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) | ||||
| @@ -127,8 +146,8 @@ class RSS(inkycal_module): | ||||
|     del filtered_feeds, parsed_feeds, wrapped, counter, text | ||||
|  | ||||
|     # Save image of black and colour channel in image-folder | ||||
|     im_black.save(images+self.name+'.png', 'PNG') | ||||
|     im_colour.save(images+self.name+'_colour.png', 'PNG') | ||||
|     return im_black, im_colour | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone/debug mode'.format(filename)) | ||||
|   print(RSS.get_config()) | ||||
|   | ||||
							
								
								
									
										41
									
								
								inkycal/modules/inkycal_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								inkycal/modules/inkycal_server.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| #!/usr/bin/python3 | ||||
| # -*- coding: utf-8 -*- | ||||
| """ | ||||
| Image Server module for Inkycal project | ||||
| For use with Robert Sierre's inkycal web-service | ||||
|  | ||||
| Copyright by aceisace | ||||
| """ | ||||
|  | ||||
| from os import path | ||||
| from PIL import ImageOps | ||||
| import requests | ||||
| import numpy | ||||
|  | ||||
| """----------------------------------------------------------------""" | ||||
| #path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' | ||||
| #path  ='/home/pi/Inky-Calendar/images/canvas.png' | ||||
| path      = inkycal_image_path | ||||
| path_body = inkycal_image_path_body | ||||
| mode = 'auto'         # 'horizontal' # 'vertical' # 'auto' | ||||
| upside_down = False    # Flip image by 180 deg (upside-down) | ||||
| alignment = 'center'  # top_center, top_left, center_left, bottom_right etc. | ||||
| colours = 'bwr'       # bwr # bwy # bw | ||||
| render = True         # show image on E-Paper? | ||||
| """----------------------------------------------------------------""" | ||||
|  | ||||
|  | ||||
| path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height)) | ||||
| print(path) | ||||
|  | ||||
| try: | ||||
|   # POST request, passing path_body in the body | ||||
|   im = Image.open(requests.post(path, json=path_body, stream=True).raw) | ||||
|    | ||||
| except FileNotFoundError: | ||||
|   raise Exception('Your file could not be found. Please check the path to your file.') | ||||
|  | ||||
| except OSError: | ||||
|   raise Exception('Please check if the path points to an image file.') | ||||
|  | ||||
|  | ||||
| @@ -20,14 +20,157 @@ logger = logging.getLogger(filename) | ||||
| logger.setLevel(level=logging.ERROR) | ||||
|  | ||||
|  | ||||
| api = todoist.TodoistAPI('your api key') | ||||
| api.sync() | ||||
| class Todoist(inkycal_module): | ||||
|   """Todoist api class | ||||
|   parses todo's from api-key | ||||
|   """ | ||||
|  | ||||
| # Print name of author | ||||
| print(api.state['user']['full_name']+'\n') | ||||
|   name = "Inkycal Todoist" | ||||
|  | ||||
|   requires = { | ||||
|     'api_key': { | ||||
|       "label":"Please enter your Todoist API-key", | ||||
|       }, | ||||
|   } | ||||
|  | ||||
|   optional = { | ||||
|     'project_filter': { | ||||
|       "label":"Show Todos only from following project (separated by a comma). Leave empty to show "+ | ||||
|       "todos from all projects", | ||||
|       "default": [] | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_rss module""" | ||||
|  | ||||
|     super().__init__(section_size, section_config) | ||||
|  | ||||
|     # Module specific parameters | ||||
|     for param in self.requires: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|  | ||||
| tasks = (task.data for task in api.state['items']) | ||||
|     # module specific parameters | ||||
|     self.api_key = self.config['api_key'] | ||||
|     self.project_filter = self.config['project_filter']# only show todos from these projects | ||||
|  | ||||
| for _ in tasks: | ||||
|   print('task: {} is {}'.format(_['content'], 'done' if _['checked'] == 1 else 'not done')) | ||||
|     self._api = todoist.TodoistAPI(self.config['api_key']) | ||||
|     self._api.sync() | ||||
|  | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|  | ||||
|   def _validate(self): | ||||
|     """Validate module-specific parameters""" | ||||
|     if not isinstance(self.api_key, str): | ||||
|       print('api_key has to be a string: "Yourtopsecretkey123" ') | ||||
|  | ||||
|   def generate_image(self): | ||||
|     """Generate image for this module""" | ||||
|  | ||||
|     # Define new image size with respect to padding | ||||
|     im_width = int(self.width - (2 * self.padding_x)) | ||||
|     im_height = int(self.height - (2 * self.padding_y)) | ||||
|     im_size = im_width, im_height | ||||
|     logger.info('image size: {} x {} px'.format(im_width, im_height)) | ||||
|  | ||||
|     # Create an image for black pixels and one for coloured pixels | ||||
|     im_black = Image.new('RGB', size = im_size, color = 'white') | ||||
|     im_colour = Image.new('RGB', size = im_size, color = 'white') | ||||
|  | ||||
|     # Check if internet is available | ||||
|     if internet_available() == True: | ||||
|       logger.info('Connection test passed') | ||||
|     else: | ||||
|       raise Exception('Network could not be reached :/') | ||||
|  | ||||
|     # Set some parameters for formatting todos | ||||
|     line_spacing = 1 | ||||
|     line_height = self.font.getsize('hg')[1] + line_spacing | ||||
|     line_width = im_width | ||||
|     max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing)) | ||||
|  | ||||
|     # Calculate padding from top so the lines look centralised | ||||
|     spacing_top = int( im_height % line_height / 2 ) | ||||
|  | ||||
|     # Calculate line_positions | ||||
|     line_positions = [ | ||||
|       (0, spacing_top + _ * line_height ) for _ in range(max_lines)] | ||||
|  | ||||
| #------------------------------------------------------------------------## | ||||
|     # Get all projects by name and id | ||||
|     all_projects = {project['name']: project['id'] | ||||
|                     for project in self._api.projects.all()} | ||||
|  | ||||
|     # Check if project from filter could be found | ||||
|     if self.project_filter: | ||||
|       for project in self.project_filter: | ||||
|         if project not in all_projects: | ||||
|           print('Could not find a project named {}'.format(project)) | ||||
|           self.project_filter.remove(project) | ||||
|  | ||||
|     # function for extracting project names from tasks | ||||
|     get_project_name = lambda task: (self._api.projects.get_data( | ||||
|                                      task['project_id'])['project']['name']) | ||||
|  | ||||
|     # If the filter is empty, parse all tasks which are not yet done | ||||
|     if self.project_filter: | ||||
|       tasks = (task.data for task in self._api.state['items'] | ||||
|                if (task['checked'] == 0) and | ||||
|                (get_project_name(task) in self.project_filter)) | ||||
|  | ||||
|     # If filter is not empty, parse undone tasks in only those projects | ||||
|     else: | ||||
|       tasks = (task.data for task in self._api.state['items'] if | ||||
|                (task['checked'] == 0)) | ||||
|  | ||||
|     # Simplify the tasks for faster processing | ||||
|     simplified = [{'name':task['content'], | ||||
|                    'due':task['due'], | ||||
|                    'priority':task['priority'], | ||||
|                    'project_id':task['project_id']} | ||||
|                   for task in tasks] | ||||
|  | ||||
|     # Group tasks by project name | ||||
|     grouped = {} | ||||
|  | ||||
|     if self.project_filter: | ||||
|       for project in self.project_filter: | ||||
|         project_id = all_projects[project] | ||||
|         grouped[ project ] = [ | ||||
|           task for task in simplified if task['project_id'] == project_id] | ||||
|     else: | ||||
|       for project in all_projects: | ||||
|         project_id = all_projects[project] | ||||
|         grouped[ project ] = [ | ||||
|           task for task in simplified if task['project_id'] == project_id] | ||||
|  | ||||
|     # Print tasks sorted by groups | ||||
|     for project, tasks in grouped.items(): | ||||
|       print('*', project) | ||||
|       for task in tasks: | ||||
|         print('• {} {}'.format( | ||||
|           task['due']['string'] if task['due'] != None else '', task['name'])) | ||||
|  | ||||
|  | ||||
| ##    # Write rss-feeds on image | ||||
| ##    for _ in range(len(filtered_feeds)): | ||||
| ##      write(im_black, line_positions[_], (line_width, line_height), | ||||
| ##            filtered_feeds[_], font = self.font, alignment= 'left') | ||||
|  | ||||
|  | ||||
|  | ||||
|     # Cleanup --------------------------- | ||||
|     # del grouped, parsed_feeds, wrapped, counter, text | ||||
|  | ||||
|     # return the images ready for the display | ||||
|     return im_black, im_colour | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone/debug mode'.format(filename)) | ||||
|   config = {'api_key':'4e166367dcafdd60e6a9f4cbed598d578bf2c359'} | ||||
|   size = (480, 100) | ||||
|   a = Todoist(size, config) | ||||
|   b,c = a.generate_image() | ||||
|   | ||||
| @@ -26,6 +26,60 @@ class Weather(inkycal_module): | ||||
|   """Weather class | ||||
|   parses weather details from openweathermap | ||||
|   """ | ||||
|   #TODO: automatic setup of pyowm by location id if location is numeric | ||||
|  | ||||
|   name = "Inkycal Weather (openweathermap)" | ||||
|  | ||||
|   requires = { | ||||
|  | ||||
|     "api_key" : { | ||||
|       "label":"Please enter openweathermap api-key. You can create one for free on openweathermap", | ||||
|     }, | ||||
|      | ||||
|     "location": { | ||||
|       "label":"Please enter your location in the following format: City, Country-Code" | ||||
|       } | ||||
|     } | ||||
|  | ||||
|   optional = { | ||||
|      | ||||
|     "round_temperature": { | ||||
|       "label":"Round temperature to the nearest degree?", | ||||
|       "options": [True, False], | ||||
|       "default" : True | ||||
|       }, | ||||
|  | ||||
|     "round_windspeed": { | ||||
|       "label":"Round windspeed?", | ||||
|       "options": [True, False], | ||||
|       "default": True | ||||
|       }, | ||||
|  | ||||
|     "forecast_interval": { | ||||
|       "label":"Please select the forecast interval", | ||||
|       "options": ["daily", "hourly"], | ||||
|       "default": "daily" | ||||
|       }, | ||||
|  | ||||
|     "units": { | ||||
|       "label": "Which units should be used?", | ||||
|       "options": ["metric", "imperial"], | ||||
|       "default": "metric" | ||||
|       }, | ||||
|  | ||||
|     "hour_format": { | ||||
|       "label": "Which hour format do you prefer?", | ||||
|       "options": [12, 24], | ||||
|       "default": 24 | ||||
|       }, | ||||
|  | ||||
|     "use_beaufort": { | ||||
|       "label": "Use beaufort scale for windspeed?", | ||||
|       "options": [True, False], | ||||
|       "default": True | ||||
|       }, | ||||
|      | ||||
|     } | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_weather module""" | ||||
| @@ -33,35 +87,36 @@ class Weather(inkycal_module): | ||||
|     super().__init__(section_size, section_config) | ||||
|  | ||||
|     # Module specific parameters | ||||
|     required = ['api_key','location'] | ||||
|     for param in required: | ||||
|     for param in self.requires: | ||||
|       if not param in section_config: | ||||
|         raise Exception('config is missing {}'.format(param)) | ||||
|  | ||||
|     # module name | ||||
|     self.name = self.__class__.__name__ | ||||
|     # required parameters | ||||
|     self.location = self.config['location'] | ||||
|     self.api_key = self.config['api_key'] | ||||
|  | ||||
|     # module specific parameters | ||||
|     self.owm = pyowm.OWM(self.config['api_key']) | ||||
|     # optional parameters | ||||
|     self.round_temperature = self.config['round_temperature'] | ||||
|     self.round_windspeed = self.config['round_windspeed'] | ||||
|     self.forecast_interval = self.config['forecast_interval'] | ||||
|     self.units = self.config['units'] | ||||
|     self.hour_format = self.config['hours'] | ||||
|     self.hour_format = self.config['hour_format'] | ||||
|     self.use_beaufort = self.config['use_beaufort'] | ||||
|     self.timezone = get_system_tz() | ||||
|     self.round_temperature = True | ||||
|     self.round_windspeed = True | ||||
|     self.use_beaufort = True | ||||
|     self.forecast_interval = 'daily' # daily # hourly | ||||
|     self.locale = sys_locale()[0] | ||||
|     self.weatherfont = ImageFont.truetype(fonts['weathericons-regular-webfont'], | ||||
|                                           size = self.fontsize) | ||||
|  | ||||
|     #self.owm = pyowm.OWM(self.config['api_key']) | ||||
|     # give an OK message | ||||
|     print('{0} loaded'.format(self.name)) | ||||
|     print('{0} loaded'.format(filename)) | ||||
|  | ||||
|   def generate_image(self): | ||||
|     """Generate image for this module""" | ||||
|  | ||||
|     # Define new image size with respect to padding | ||||
|     im_width = int(self.width - (self.width * 2 * self.margin_x)) | ||||
|     im_height = int(self.height - (self.height * 2 * self.margin_y)) | ||||
|     im_width = int(self.width - (2 * self.padding_x)) | ||||
|     im_height = int(self.height - (2 * self.padding_y)) | ||||
|     im_size = im_width, im_height | ||||
|     logger.info('image size: {} x {} px'.format(im_width, im_height)) | ||||
|  | ||||
| @@ -422,9 +477,8 @@ class Weather(inkycal_module): | ||||
|     draw_border(im_black, (col6, row1), (col_width, im_height)) | ||||
|     draw_border(im_black, (col7, row1), (col_width, im_height)) | ||||
|  | ||||
|   # Save image of black and colour channel in image-folder | ||||
|     im_black.save(images+self.name+'.png', "PNG") | ||||
|     im_colour.save(images+self.name+'_colour.png', "PNG") | ||||
|     # return the images ready for the display | ||||
|     return im_black, im_colour | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|   print('running {0} in standalone mode'.format(filename)) | ||||
|   | ||||
| @@ -10,14 +10,16 @@ class inkycal_module(metaclass=abc.ABCMeta): | ||||
|       callable(subclass.generate_image) or | ||||
|       NotImplemented) | ||||
|  | ||||
|   def __init__(self, section_size, section_config): | ||||
|   def __init__(self, section_config): | ||||
|     # Initializes base module | ||||
|     # sets properties shared amongst all sections | ||||
|     self.config = section_config | ||||
|     self.width, self.height = section_size | ||||
|     self.fontsize = 12 | ||||
|     self.margin_x = 0.02 | ||||
|     self.margin_y = 0.05 | ||||
|     self.width, self.height = section_config['size'] | ||||
|  | ||||
|     self.padding_left = self.padding_right = self.config["padding_x"] | ||||
|     self.padding_top = self.padding_bottom = self.config["padding_y"] | ||||
|  | ||||
|     self.fontsize = self.config["fontsize"] | ||||
|     self.font = ImageFont.truetype( | ||||
|       fonts['NotoSans-SemiCondensed'], size = self.fontsize) | ||||
|  | ||||
| @@ -56,3 +58,33 @@ class inkycal_module(metaclass=abc.ABCMeta): | ||||
|     # Generate image for this module with specified parameters | ||||
|     raise NotImplementedError( | ||||
|       'The developers were too lazy to implement this function') | ||||
|  | ||||
|   @classmethod | ||||
|   def get_config(cls): | ||||
|     # Get the config of this module for the web-ui | ||||
|     # Do not change | ||||
|     try: | ||||
|  | ||||
|       if hasattr(cls, 'requires'): | ||||
|         for each in cls.requires: | ||||
|           if not "label" in cls.requires[each]: | ||||
|             raise Exception("no label found for {}".format(each)) | ||||
|  | ||||
|       if hasattr(cls, 'optional'): | ||||
|         for each in cls.optional: | ||||
|           if not "label" in cls.optional[each]: | ||||
|             raise Exception("no label found for {}".format(each)) | ||||
|  | ||||
|       conf = { | ||||
|         "name": cls.__name__, | ||||
|         "name_str": cls.name, | ||||
|         "requires": cls.requires if hasattr(cls, 'requires') else {}, | ||||
|         "optional": cls.optional if hasattr(cls, 'optional') else {}, | ||||
|         } | ||||
|       return conf | ||||
|     except: | ||||
|       raise Exception( | ||||
|         'Ohoh, something went wrong while trying to get the config of this module') | ||||
|  | ||||
|  | ||||
|    | ||||
|   | ||||
| @@ -60,9 +60,21 @@ class Simple(inkycal_module): | ||||
|   Explain what this module does... | ||||
|   """ | ||||
|  | ||||
|   # name is the name that will be shown on the web-ui | ||||
|   # may be same or different to the class name (Do not remove this) | ||||
|   name = "My own module" | ||||
|  | ||||
|   # create a dictionary that specifies what your module absolutely needs | ||||
|   # to run correctly | ||||
|   # Use the following format -> "key" : "info about this key for web-ui" | ||||
|   # You can add as many required entries as you like | ||||
|   requires = { | ||||
|     "module_parameter" : "Short info about this parameter, shown on the web-ui", | ||||
|     } | ||||
|  | ||||
|   # Initialise the class (do not remove) | ||||
|   def __init__(self, section_size, section_config): | ||||
|     """Initialize inkycal_rss module""" | ||||
|     """Initialize your module module""" | ||||
|  | ||||
|     # Initialise this module via the inkycal_module template (required) | ||||
|     super().__init__(section_size, section_config) | ||||
|   | ||||
							
								
								
									
										474
									
								
								inkycal/old.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										474
									
								
								inkycal/old.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,474 @@ | ||||
| from inkycal import Settings, Layout | ||||
| from inkycal.custom import * | ||||
|  | ||||
| #from os.path import exists | ||||
| import os | ||||
| import traceback | ||||
| import logging | ||||
| import arrow | ||||
| import time | ||||
|  | ||||
| try: | ||||
|   from PIL import Image | ||||
| except ImportError: | ||||
|   print('Pillow is not installed! Please install with:') | ||||
|   print('pip3 install Pillow') | ||||
|  | ||||
| try: | ||||
|   import numpy | ||||
| except ImportError: | ||||
|   print('numpy is not installed! Please install with:') | ||||
|   print('pip3 install numpy') | ||||
|  | ||||
| logger = logging.getLogger('inkycal') | ||||
| logger.setLevel(level=logging.ERROR) | ||||
|  | ||||
| class Inkycal: | ||||
|   """Inkycal main class""" | ||||
|  | ||||
|   def __init__(self, settings_path, render=True): | ||||
|     """initialise class | ||||
|     settings_path = str -> location/folder of settings file | ||||
|     render = bool -> show something on the ePaper? | ||||
|     """ | ||||
|     self._release = '2.0.0beta' | ||||
|  | ||||
|     # Check if render is boolean | ||||
|     if not isinstance(render, bool): | ||||
|       raise Exception('render must be True or False, not "{}"'.format(render)) | ||||
|     self.render = render | ||||
|  | ||||
|     # Init settings class | ||||
|     self.Settings = Settings(settings_path) | ||||
|  | ||||
|     # Check if display support colour | ||||
|     self.supports_colour = self.Settings.Layout.supports_colour | ||||
|  | ||||
|     # Option to flip image upside down | ||||
|     if self.Settings.display_orientation == 'normal': | ||||
|       self.upside_down = False | ||||
|  | ||||
|     elif self.Settings.display_orientation == 'upside_down': | ||||
|       self.upside_down = True | ||||
|  | ||||
|     # Option to use epaper image optimisation | ||||
|     self.optimize = True | ||||
|  | ||||
|     # Load drivers if image should be rendered | ||||
|     if self.render == True: | ||||
|  | ||||
|       # Get model and check if colour can be rendered | ||||
|       model= self.Settings.model | ||||
|  | ||||
|       # Init Display class | ||||
|       from inkycal.display import Display | ||||
|       self.Display = Display(model) | ||||
|  | ||||
|       # get calibration hours | ||||
|       self._calibration_hours = self.Settings.calibration_hours | ||||
|  | ||||
|       # set a check for calibration | ||||
|       self._calibration_state = False | ||||
|  | ||||
|     # load+validate settings file. Import and setup specified modules | ||||
|     self.active_modules = self.Settings.active_modules() | ||||
|     for module in self.active_modules: | ||||
|       try: | ||||
|         loader = 'from inkycal.modules import {0}'.format(module) | ||||
|         module_data = self.Settings.get_config(module) | ||||
|         size, conf = module_data['size'], module_data['config'] | ||||
|         setup = 'self.{} = {}(size, conf)'.format(module, module) | ||||
|         exec(loader) | ||||
|         exec(setup) | ||||
|         logger.debug(('{}: size: {}, config: {}'.format(module, size, conf))) | ||||
|  | ||||
|       # If a module was not found, print an error message | ||||
|       except ImportError: | ||||
|         print( | ||||
|           'Could not find module: "{}". Please try to import manually.'.format( | ||||
|           module)) | ||||
|  | ||||
|     # Give an OK message | ||||
|     print('loaded inkycal') | ||||
|  | ||||
|   def countdown(self, interval_mins=None): | ||||
|     """Returns the remaining time in seconds until next display update""" | ||||
|  | ||||
|     # Validate update interval | ||||
|     allowed_intervals = [10, 15, 20, 30, 60] | ||||
|  | ||||
|     # Check if empty, if empty, use value from settings file | ||||
|     if interval_mins == None: | ||||
|       interval_mins = self.Settings.update_interval | ||||
|  | ||||
|     # Check if integer | ||||
|     if not isinstance(interval_mins, int): | ||||
|       raise Exception('Update interval must be an integer -> 60') | ||||
|  | ||||
|     # Check if value is supported | ||||
|     if interval_mins not in allowed_intervals: | ||||
|       raise Exception('Update interval is {}, but should be one of: {}'.format( | ||||
|         interval_mins, allowed_intervals)) | ||||
|  | ||||
|     # Find out at which minutes the update should happen | ||||
|     now = arrow.now() | ||||
|     update_timings = [(60 - int(interval_mins)*updates) for updates in | ||||
|                       range(60//int(interval_mins))][::-1] | ||||
|  | ||||
|     # Calculate time in mins until next update | ||||
|     minutes = [_ for _ in update_timings if _>= now.minute][0] - now.minute | ||||
|  | ||||
|     # Print the remaining time in mins until next update | ||||
|     print('{0} Minutes left until next refresh'.format(minutes)) | ||||
|  | ||||
|     # Calculate time in seconds until next update | ||||
|     remaining_time = minutes*60 + (60 - now.second) | ||||
|  | ||||
|     # Return seconds until next update | ||||
|     return remaining_time | ||||
|  | ||||
|   def test(self): | ||||
|     """Inkycal test run. | ||||
|     Generates images for each module, one by one and prints OK if no | ||||
|     problems were found.""" | ||||
|     print('You are running inkycal v{}'.format(self._release)) | ||||
|  | ||||
|  | ||||
|     print('Running inkycal test-run for {} ePaper'.format( | ||||
|       self.Settings.model)) | ||||
|  | ||||
|     if self.upside_down == True: | ||||
|       print('upside-down mode active') | ||||
|  | ||||
|     for module in self.active_modules: | ||||
|       generate_im = 'self.{0}.generate_image()'.format(module) | ||||
|       print('generating image for {} module...'.format(module), end = '') | ||||
|       try: | ||||
|         exec(generate_im) | ||||
|         print('OK!') | ||||
|       except Exception as Error: | ||||
|         print('Error!') | ||||
|         print(traceback.format_exc()) | ||||
|  | ||||
|   def run(self): | ||||
|     """Runs the main inykcal program nonstop (cannot be stopped anymore!) | ||||
|     Will show something on the display if render was set to True""" | ||||
|  | ||||
|     # TODO: printing traceback on display (or at least a smaller message?) | ||||
|     # Calibration | ||||
|  | ||||
|     # Get the time of initial run | ||||
|     runtime = arrow.now() | ||||
|  | ||||
|     # Function to flip images upside down | ||||
|     upside_down = lambda image: image.rotate(180, expand=True) | ||||
|  | ||||
|     # Count the number of times without any errors | ||||
|     counter = 1 | ||||
|  | ||||
|     # Calculate the max. fontsize for info-section | ||||
|     if self.Settings.info_section == True: | ||||
|       info_section_height = round(self.Settings.Layout.display_height* (1/95) ) | ||||
|       self.font = auto_fontsize(ImageFont.truetype( | ||||
|         fonts['NotoSans-SemiCondensed']), info_section_height) | ||||
|  | ||||
|     while True: | ||||
|       print('Generating images for all modules...') | ||||
|       for module in self.active_modules: | ||||
|         generate_im = 'self.{0}.generate_image()'.format(module) | ||||
|         try: | ||||
|           exec(generate_im) | ||||
|         except Exception as Error: | ||||
|           print('Error!') | ||||
|           message = traceback.format_exc() | ||||
|           print(message) | ||||
|           counter = 0 | ||||
|       print('OK') | ||||
|  | ||||
|       # Assemble image from each module | ||||
|       self._assemble() | ||||
|  | ||||
|       # Check if image should be rendered | ||||
|       if self.render == True: | ||||
|         Display = self.Display | ||||
|  | ||||
|         self._calibration_check() | ||||
|  | ||||
|         if self.supports_colour == True: | ||||
|           im_black = Image.open(images+'canvas.png') | ||||
|           im_colour = Image.open(images+'canvas_colour.png') | ||||
|  | ||||
|           # Flip the image by 180° if required | ||||
|           if self.upside_down == True: | ||||
|             im_black = upside_down(im_black) | ||||
|             im_colour = upside_down(im_colour) | ||||
|  | ||||
|           # render the image on the display | ||||
|           Display.render(im_black, im_colour) | ||||
|  | ||||
|         # Part for black-white ePapers | ||||
|         elif self.supports_colour == False: | ||||
|  | ||||
|           im_black = self._merge_bands() | ||||
|  | ||||
|           # Flip the image by 180° if required | ||||
|           if self.upside_down == True: | ||||
|             im_black = upside_down(im_black) | ||||
|  | ||||
|           Display.render(im_black) | ||||
|  | ||||
|       print('\ninkycal has been running without any errors for', end = ' ') | ||||
|       print('{} display updates'.format(counter)) | ||||
|       print('Programm started {}'.format(runtime.humanize())) | ||||
|  | ||||
|       counter += 1 | ||||
|  | ||||
|       sleep_time = self.countdown() | ||||
|       time.sleep(sleep_time) | ||||
|  | ||||
|   def _merge_bands(self): | ||||
|     """Merges black and coloured bands for black-white ePapers | ||||
|     returns the merged image | ||||
|     """ | ||||
|  | ||||
|     im_path = images | ||||
|  | ||||
|     im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png' | ||||
|  | ||||
|     # If there is an image for black and colour, merge them | ||||
|     if os.path.exists(im1_path) and os.path.exists(im2_path): | ||||
|  | ||||
|       im1 = Image.open(im1_path).convert('RGBA') | ||||
|       im2 = Image.open(im2_path).convert('RGBA') | ||||
|  | ||||
|       def clear_white(img): | ||||
|         """Replace all white pixels from image with transparent pixels | ||||
|         """ | ||||
|         x = numpy.asarray(img.convert('RGBA')).copy() | ||||
|         x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8) | ||||
|         return Image.fromarray(x) | ||||
|  | ||||
|       im2 = clear_white(im2) | ||||
|       im1.paste(im2, (0,0), im2) | ||||
|  | ||||
|     # If there is no image for the coloured-band, return the bw-image | ||||
|     elif os.path.exists(im1_path) and not os.path.exists(im2_path): | ||||
|       im1 = Image.open(im1_name).convert('RGBA') | ||||
|  | ||||
|     return im1 | ||||
|  | ||||
|  | ||||
|   def _assemble(self): | ||||
|     """Assmebles all sub-images to a single image""" | ||||
|  | ||||
|     # Create an empty canvas with the size of the display | ||||
|     width, height = self.Settings.Layout.display_size | ||||
|      | ||||
|     if self.Settings.info_section == True: | ||||
|       height = round(height * ((1/95)*100) ) | ||||
|  | ||||
|     im_black = Image.new('RGB', (width, height), color = 'white') | ||||
|     im_colour = Image.new('RGB', (width ,height), color = 'white') | ||||
|  | ||||
|     # Set cursor for y-axis | ||||
|     im1_cursor = 0 | ||||
|     im2_cursor = 0 | ||||
|  | ||||
|     for module in self.active_modules: | ||||
|  | ||||
|       im1_path = images+module+'.png' | ||||
|       im2_path = images+module+'_colour.png' | ||||
|  | ||||
|       # Check if there is an image for the black band | ||||
|       if os.path.exists(im1_path): | ||||
|  | ||||
|         # Get actual size of image | ||||
|         im1 = Image.open(im1_path).convert('RGBA') | ||||
|         im1_size = im1.size | ||||
|  | ||||
|         # Get the size of the section | ||||
|         section_size = self.Settings.get_config(module)['size'] | ||||
|         # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0] - im1_size[0]) /2) | ||||
|  | ||||
|         # If this is the first module, use the y-offset | ||||
|         if im1_cursor == 0: | ||||
|           y = int( (section_size[1]-im1_size[1]) /2) | ||||
|         else: | ||||
|           y = im1_cursor + int( (section_size[1]-im1_size[1]) /2) | ||||
|  | ||||
|         # center the image in the section space | ||||
|         im_black.paste(im1, (x,y), im1) | ||||
|  | ||||
|         # Shift the y-axis cursor at the beginning of next section | ||||
|         im1_cursor += section_size[1] | ||||
|  | ||||
|       # Check if there is an image for the coloured band | ||||
|       if os.path.exists(im2_path): | ||||
|  | ||||
|         # Get actual size of image | ||||
|         im2 = Image.open(im2_path).convert('RGBA') | ||||
|         im2_size = im2.size | ||||
|  | ||||
|         # Get the size of the section | ||||
|         section_size = self.Settings.get_config(module)['size'] | ||||
|  | ||||
|         # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0]-im2_size[0]) /2) | ||||
|  | ||||
|         # If this is the first module, use the y-offset | ||||
|         if im2_cursor == 0: | ||||
|           y = int( (section_size[1]-im2_size[1]) /2) | ||||
|         else: | ||||
|           y = im2_cursor + int( (section_size[1]-im2_size[1]) /2) | ||||
|  | ||||
|         # center the image in the section space | ||||
|         im_colour.paste(im2, (x,y), im2) | ||||
|  | ||||
|         # Shift the y-axis cursor at the beginning of next section | ||||
|         im2_cursor += section_size[1] | ||||
|  | ||||
|     # Show an info section if specified by the settings file | ||||
|     now = arrow.now() | ||||
|     stamp = 'last update: {}'.format(now.format('D MMM @ HH:mm', locale = | ||||
|                                                 self.Settings.language)) | ||||
|     if self.Settings.info_section == True: | ||||
|       write(im_black, (0, im1_cursor), (width, height-im1_cursor), | ||||
|             stamp, font = self.font) | ||||
|  | ||||
|  | ||||
|     # optimize the image by mapping colours to pure black and white | ||||
|     if self.optimize == True: | ||||
|       self._optimize_im(im_black).save(images+'canvas.png', 'PNG') | ||||
|       self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG') | ||||
|     else: | ||||
|       im_black.save(images+'canvas.png', 'PNG') | ||||
|       im_colour.save(images+'canvas_colour.png', 'PNG') | ||||
|  | ||||
|   def _optimize_im(self, image, threshold=220): | ||||
|     """Optimize the image for rendering on ePaper displays""" | ||||
|  | ||||
|     buffer = numpy.array(image.convert('RGB')) | ||||
|     red, green = buffer[:, :, 0], buffer[:, :, 1] | ||||
|     # grey->black | ||||
|     buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] | ||||
|     image = Image.fromarray(buffer) | ||||
|     return image | ||||
|  | ||||
|   def calibrate(self): | ||||
|     """Calibrate the ePaper display to prevent burn-ins (ghosting) | ||||
|     use this command to manually calibrate the display""" | ||||
|  | ||||
|     self.Display.calibrate() | ||||
|  | ||||
|   def _calibration_check(self): | ||||
|     """Calibration sheduler | ||||
|     uses calibration hours from settings file to check if calibration is due""" | ||||
|     now = arrow.now() | ||||
|     print('hour:', now.hour, 'hours:', self._calibration_hours) | ||||
|     print('state:', self._calibration_state) | ||||
|     if now.hour in self._calibration_hours and self._calibration_state == False: | ||||
|       self.calibrate() | ||||
|       self._calibration_state = True | ||||
|     else: | ||||
|       self._calibration_state = False | ||||
|  | ||||
|  | ||||
|   def _check_for_updates(self): | ||||
|     """Check if a new update is available for inkycal""" | ||||
|  | ||||
|     raise NotImplementedError('Tha developer were too lazy to implement this..') | ||||
|  | ||||
|  | ||||
|   @staticmethod | ||||
|   def _add_module(filepath_module, classname): | ||||
|     """Add a third party module to inkycal | ||||
|     filepath_module = the full path of your module. The file should be in /modules! | ||||
|     classname = the name of your class inside the module | ||||
|     """ | ||||
|  | ||||
|     # Path for modules | ||||
|     _module_path = 'inkycal/modules/' | ||||
|  | ||||
|     # Check if the filepath is a string | ||||
|     if not isinstance(filepath_module, str): | ||||
|       raise ValueError('filepath has to be a string!') | ||||
|  | ||||
|     # Check if the classname is a string | ||||
|     if not isinstance(classname, str): | ||||
|       raise ValueError('classname has to be a string!') | ||||
|  | ||||
|     # TODO: | ||||
|     # Ensure only third-party modules are deleted as built-in modules | ||||
|     # should not be deleted | ||||
|  | ||||
|     # Check if module is inside the modules folder | ||||
|     if not _module_path in filepath_module: | ||||
|       raise Exception('Your module should be in', _module_path) | ||||
|  | ||||
|     # Get the name of the third-party module file without extension (.py) | ||||
|     filename = filepath_module.split('.py')[0].split('/')[-1] | ||||
|  | ||||
|     # Check if filename or classname is in the current module init file | ||||
|     with open('modules/__init__.py', mode ='r') as module_init: | ||||
|       content = module_init.read().splitlines() | ||||
|  | ||||
|     for line in content: | ||||
|       if (filename or clasname) in line: | ||||
|         raise Exception( | ||||
|           'A module with this filename or classname already exists') | ||||
|  | ||||
|     # Check if filename or classname is in the current inkycal init file | ||||
|     with open('__init__.py', mode ='r') as inkycal_init: | ||||
|       content = inkycal_init.read().splitlines() | ||||
|  | ||||
|     for line in content: | ||||
|       if (filename or clasname) in line: | ||||
|         raise Exception( | ||||
|           'A module with this filename or classname already exists') | ||||
|  | ||||
|     # If all checks have passed, add the module in the module init file | ||||
|     with open('modules/__init__.py', mode='a') as module_init: | ||||
|       module_init.write('from .{} import {}'.format(filename, classname)) | ||||
|  | ||||
|     # If all checks have passed, add the module in the inkycal init file | ||||
|     with open('__init__.py', mode ='a') as inkycal_init: | ||||
|       inkycal_init.write('# Added by module adder \n') | ||||
|       inkycal_init.write('import inkycal.modules.{}'.format(filename)) | ||||
|  | ||||
|     print('Your module {} has been added successfully! Hooray!'.format( | ||||
|       classname)) | ||||
|  | ||||
|   @staticmethod | ||||
|   def _remove_module(classname, remove_file = True): | ||||
|     """Removes a third-party module from inkycal | ||||
|     Input the classname of the file you want to remove  | ||||
|     """ | ||||
|  | ||||
|     # Check if filename or classname is in the current module init file | ||||
|     with open('modules/__init__.py', mode ='r') as module_init: | ||||
|       content = module_init.read().splitlines() | ||||
|  | ||||
|     with open('modules/__init__.py', mode ='w') as module_init: | ||||
|       for line in content: | ||||
|         if not classname in line: | ||||
|           module_init.write(line+'\n') | ||||
|         else: | ||||
|           filename = line.split(' ')[1].split('.')[1] | ||||
|  | ||||
|     # Check if filename or classname is in the current inkycal init file | ||||
|     with open('__init__.py', mode ='r') as inkycal_init: | ||||
|       content = inkycal_init.read().splitlines() | ||||
|  | ||||
|     with open('__init__.py', mode ='w') as inkycal_init: | ||||
|       for line in content: | ||||
|         if not filename in line: | ||||
|           inkycal_init.write(line+'\n') | ||||
|  | ||||
|     # remove the file of the third party module if it exists and remove_file | ||||
|     # was set to True (default) | ||||
|     if os.path.exists('modules/{}.py'.format(filename)) and remove_file == True: | ||||
|       os.remove('modules/{}.py'.format(filename)) | ||||
|  | ||||
|     print('The module {} has been removed successfully'.format(classname)) | ||||
|  | ||||
							
								
								
									
										62
									
								
								inkycal/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								inkycal/settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| { | ||||
|     "model": "epd_7_in_5_v3", | ||||
|     "update_interval": 60, | ||||
|     "orientation": 0, | ||||
|     "info_section": false, | ||||
|     "calibration_hours": [ | ||||
|         0, | ||||
|         12, | ||||
|         18 | ||||
|     ], | ||||
|     "modules": [ | ||||
|         { | ||||
|             "position": 1, | ||||
|             "name": "Weather", | ||||
|             "height": 10, | ||||
|             "config": { | ||||
|                 "api_key": "57c07b8f2ae09e348d32317f1bfe3f52", | ||||
|                 "location": "Stuttgart,DE", | ||||
|                 "round_temperature": "True", | ||||
|                 "round_windspeed": "True", | ||||
|                 "forecast_interval": "daily", | ||||
|                 "units": "metric", | ||||
|                 "hour_format": "24", | ||||
|                 "use_beaufort": "True" | ||||
|             }, | ||||
|             "padding_x": 10, | ||||
|             "padding_y": 10, | ||||
|             "fontsize": 12, | ||||
|             "language": "en" | ||||
|         }, | ||||
|         { | ||||
|             "position": 2, | ||||
|             "name": "Calendar", | ||||
|             "height": 65, | ||||
|             "config": { | ||||
|                 "week_starts_on": "Monday", | ||||
|                 "show_events": "True", | ||||
|                 "ical_urls": [], | ||||
|                 "ical_files": [], | ||||
|                 "date_format": "D MMM", | ||||
|                 "time_format": "HH:mm" | ||||
|             }, | ||||
|             "padding_x": 10, | ||||
|             "padding_y": 10, | ||||
|             "fontsize": 12, | ||||
|             "language": "en" | ||||
|         }, | ||||
|         { | ||||
|             "position": 3, | ||||
|             "name": "RSS", | ||||
|             "height": 25, | ||||
|             "config": { | ||||
|                 "rss_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#", | ||||
|                 "shuffle_feeds": "True" | ||||
|             }, | ||||
|             "padding_x": 10, | ||||
|             "padding_y": 10, | ||||
|             "fontsize": 12, | ||||
|             "language": "en" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| @@ -5,4 +5,6 @@ recurring-ical-events==0.1.17b0 # parse recurring events | ||||
| feedparser==5.2.1               # parse RSS-feeds | ||||
| numpy>=1.18.2                   # image pre-processing #pre-installed on Raspbian, omitting | ||||
| arrow>=0.15.6 	                # time operations | ||||
| jsmin>=2.2.2                    # parsing settings.jsonc file | ||||
| #jsmin>=2.2.2                    # parsing settings.jsonc file | ||||
| flask==1.1.2					# required for web-ui | ||||
| Flask-WTF==0.14.3				# required for web-ui | ||||
							
								
								
									
										7
									
								
								server/app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/app/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| from flask import Flask | ||||
| from config import Config | ||||
|  | ||||
| app = Flask(__name__) | ||||
| app.config.from_object(Config) | ||||
|  | ||||
| from app import routes | ||||
							
								
								
									
										16
									
								
								server/app/config_loader.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/app/config_loader.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| from inkycal.modules import * | ||||
|  | ||||
| # get list of all modules inside inkycal-modules folder | ||||
| modules = [i for i in dir() if i[0].isupper()] | ||||
|  | ||||
| # Add the config of each module to the list settings | ||||
| settings = [] | ||||
|  | ||||
| for module in modules: | ||||
|     command = f"conf = {module}.get_config()" | ||||
|     exec(command) | ||||
|     settings.append(conf) | ||||
|  | ||||
| # return the config of all modules for the web-ui | ||||
| def get_all_config(): | ||||
|     return settings | ||||
							
								
								
									
										12
									
								
								server/app/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/app/forms.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| from flask_wtf import FlaskForm | ||||
| from wtforms import BooleanField | ||||
|  | ||||
| #from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField | ||||
| #from wtforms.validators import DataRequired | ||||
|  | ||||
|  | ||||
| class LoginForm(FlaskForm): | ||||
|     #username = StringField('api-key', validators=[DataRequired()]) | ||||
|     #modules = SelectField(u'modules', choices = [(_[0], _[1]) for _ in modules]) | ||||
|     remember_me = BooleanField('Show info section') | ||||
|     #submit = SubmitField('Sign In') | ||||
							
								
								
									
										108
									
								
								server/app/routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								server/app/routes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| from flask import render_template, flash, redirect, request, Response | ||||
| from app import app | ||||
| from app.forms import LoginForm | ||||
| import json | ||||
|  | ||||
| from inkycal import Display | ||||
|  | ||||
| from .config_loader import get_all_config | ||||
|  | ||||
| settings = get_all_config() | ||||
|  | ||||
| # Home | ||||
| @app.route('/') | ||||
| @app.route('/index') | ||||
| def index(): | ||||
|     return render_template('index.html', title='Home') | ||||
|  | ||||
| # Wifi-setup | ||||
| @app.route('/setup_wifi') | ||||
| def wifi_setup(): | ||||
|     return render_template('wifi.html', title='Wifi-setup') | ||||
|  | ||||
|  | ||||
| # Inkycal-setup | ||||
| @app.route('/inkycal_config', methods=['GET', 'POST']) | ||||
|  | ||||
| def inkycal_config(): | ||||
|     form = LoginForm() | ||||
|     if form.validate_on_submit(): | ||||
|  | ||||
|         # General epaper settings | ||||
|         model = request.form.get('model') | ||||
|         update_interval = int(request.form.get('update_interval')) | ||||
|         calibration_hour_1 = int(request.form.get('calibration_hour_1')) | ||||
|         calibration_hour_2 = int(request.form.get('calibration_hour_2')) | ||||
|         calibration_hour_3 = int(request.form.get('calibration_hour_3')) | ||||
|         orientation: int(request.form.get('orientation')) | ||||
|         language = request.form.get('language') | ||||
|         info_section = True if (request.form.get('info_section') == "on") else False | ||||
|  | ||||
|         # template for basic settings | ||||
|         template = { | ||||
|             "model": model, | ||||
|             "update_interval": update_interval, | ||||
|             "orientation": int(request.form.get('orientation')), | ||||
|             "info_section": info_section, | ||||
|             "calibration_hours": [calibration_hour_1, calibration_hour_2, calibration_hour_3], | ||||
|             "modules": [], | ||||
|             } | ||||
|  | ||||
|         # common module config (shared by all modules) | ||||
|         padding_x = int(request.form.get('padding_x')) | ||||
|         padding_y = int(request.form.get('padding_y')) | ||||
|         fontsize = int(request.form.get('fontsize')) | ||||
|         language = request.form.get('language') | ||||
|  | ||||
|         common_settings = {'padding_x':padding_x, 'padding_y':padding_y, 'fontsize':fontsize, 'language':language} | ||||
|  | ||||
|         # display size | ||||
|         display_size = Display.get_display_size(model) | ||||
|         width, height = display_size[0], display_size[1] | ||||
|          | ||||
|  | ||||
|         # loop over the modules, add their config data based on user selection, merge the common_settings into each module's config | ||||
|         for i in range(1,4): | ||||
|             conf = {} | ||||
|             module = 'module'+str(i) | ||||
|             if request.form.get(module) != "None": | ||||
|                 #conf = {"position":i , "name": request.form.get(module), "height": int(request.form.get(module+'_height')), "config":{}} | ||||
|                 conf = {"position":i , "name": request.form.get(module), "size": (width, int(height*int(request.form.get(module+'_height')) /100)), "config":{}} | ||||
|  | ||||
|                 for modules in settings: | ||||
|                     if modules['name'] == request.form.get(module): | ||||
|  | ||||
|                         # Add required fields to the config of the module in question | ||||
|                         if 'requires' in modules: | ||||
|                             for key in modules['requires']: | ||||
|                                 conf['config'][key] = request.form.get(module+'_'+key).replace(" ", "") | ||||
|  | ||||
|                         # For optional fields, check if user entered/selected something. If not, and a default value was given, | ||||
|                         # use the default value, else set the value of that optional key as None | ||||
|                         if 'optional' in modules: | ||||
|                             for key in modules['optional']: | ||||
|                                 if request.form.get(module+'_'+key): | ||||
|                                     conf['config'][key] = request.form.get(module+'_'+key).replace(" ", "") | ||||
|                                 else: | ||||
|                                     if "default" in modules["optional"][key]: | ||||
|                                         conf['config'][key] = modules["optional"][key]["default"] | ||||
|                                     else: | ||||
|                                         conf['config'][key] = None | ||||
|  | ||||
|                 # update the config dictionary | ||||
|                 conf.update(common_settings) | ||||
|                 template['modules'].append(conf) | ||||
|  | ||||
|         # Send the data back to the server side in json dumps and convert the response to a downloadable settings.json file | ||||
|         try: | ||||
|             user_settings = json.dumps(template, indent=4).encode('utf-8') | ||||
|             response = Response(user_settings, mimetype="application/json", direct_passthrough=True) | ||||
|             response.headers['Content-Disposition'] = 'attachment; filename=settings.json' | ||||
|  | ||||
|             return response | ||||
|             # redirect('/index') | ||||
|  | ||||
|         except Exception as e: | ||||
|             flash(str(e)) | ||||
|  | ||||
|     return render_template('inkycal_config.html', title='Inkycal-Setup', conf=settings, form=form) | ||||
							
								
								
									
										7
									
								
								server/app/static/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/app/static/css/main.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										59
									
								
								server/app/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								server/app/templates/base.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|  | ||||
|     <head> | ||||
|         <!-- Required meta tags --> | ||||
|         <meta charset="utf-8"> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|  | ||||
|         <!-- Bootstrap CSS --> | ||||
|         <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> | ||||
|  | ||||
|         {% if title %} <title>{{ title }}</title> | ||||
|         {% else %} <title>Inkycal</title> {% endif %} | ||||
|  | ||||
|         <style>     body { background-color: #eaeaea; }     </style> | ||||
|  | ||||
|     </head> | ||||
|  | ||||
|  | ||||
|     <body> | ||||
|         <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> | ||||
|         <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ho+j7jyWK8fNQe+A12Hb8AhRq26LrZ/JpcUGGOn+Y7RsweNrtN/tE3MoK7ZeZDyx" crossorigin="anonymous"></script> | ||||
|  | ||||
|         <div class="container"> | ||||
|             <div class="card text-center"> | ||||
|                 <div class="card-header"> | ||||
|                     <ul class="nav nav-pills card-header-pills"> | ||||
|  | ||||
|                         <li class="nav-item"> | ||||
|                             <a class="nav-link" href="/index">Home</a> | ||||
|                         </li> | ||||
|  | ||||
|                         <li class="nav-item"> | ||||
|                             <a class="nav-link" href="/inkycal_config">Setup</a> | ||||
|                         </li> | ||||
|                         <li class="nav-item"> | ||||
|                             <a class="nav-link" href="/setup_wifi">WiFi-setup</a> | ||||
|                         </li> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <!-- show flashed messages--> | ||||
|             <hr> | ||||
|                 {% with messages = get_flashed_messages() %} | ||||
|                 {% if messages %} | ||||
|                 <ul> | ||||
|                     {% for message in messages %} | ||||
|                     <li>{{ message }}</li> | ||||
|                     {% endfor %} | ||||
|                 </ul> | ||||
|                     {% endif %} | ||||
|                     {% endwith %} | ||||
|             </hr> | ||||
|       </div> | ||||
|  | ||||
|         {% block content %}{% endblock %} | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										9
									
								
								server/app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
|     <body> | ||||
|         <div class="container"><h4>Welcome to inkycal config portal</h4></div> | ||||
|     </body> | ||||
|  | ||||
| {% endblock %} | ||||
							
								
								
									
										445
									
								
								server/app/templates/inkycal_config.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								server/app/templates/inkycal_config.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,445 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| <!-- Main container --> | ||||
| {% block content %} | ||||
|  | ||||
| <!-- Wrap everything in a container--> | ||||
| <div class="container"> | ||||
|  | ||||
| <!-- heading --> | ||||
| <h3>Inkycal-Setup v.2.0.0 BETA</h3> | ||||
|  | ||||
| <!-- project link--> | ||||
| <div class="alert alert-light" role="alert"> | ||||
|   <a href="https://github.com/aceisace/Inky-Calendar">For Inkycal Project of ace innovation laboratory - aceinnolab.com - by aceisace</a><br> | ||||
| </div> | ||||
|  | ||||
| <!-- Inkycal logo --> | ||||
| <img class="img-fluid" src="https://github.com/aceisace/Inky-Calendar/blob/dev_ver2_0/Gallery/logo.png?raw=true" alt="Inkycal Logo"> | ||||
|  | ||||
| <br><br> | ||||
|  | ||||
| <!-- Instructions --> | ||||
| <div class="alert alert-primary" role="alert"> | ||||
|     <h4 class="alert-heading">Instructions</h4> | ||||
|     Insert your personal details and preferences and click on 'Generate'.<br> | ||||
|     Copy the downloaded file to the Raspberry Pi.<br> | ||||
|     The location does not matter, however, you need to know the path to this file.<br> | ||||
|     <hr> | ||||
|     <p class="mb-0">If no value is filled in for any of the row, the default value will be used.</p> | ||||
| </div> | ||||
|  | ||||
| <!-- Main form --> | ||||
| <form class="needs-validation" method="post" novalidate> | ||||
|     {{ form.hidden_tag() }} | ||||
|  | ||||
|     <h4> General settings </h4> | ||||
|  | ||||
|     <!-- group E-Paper settings in a single row--> | ||||
|     <div class="form-row"> | ||||
|  | ||||
|     <!-- model selection start--> | ||||
|     <div class="col"> | ||||
|         <label for="model">Model</label> | ||||
|         <select class="form-control" id="model" name="model"> | ||||
|  | ||||
|             <option value="9_in_7">                     9.7" ePaper                         </option> | ||||
|  | ||||
|             <option value="epd_7_in_5_v3_colour">       7.5" v3 (880x528px) colour          </option> | ||||
|             <option value="epd_7_in_5_v3" selected>     7.5" v3 (880x528px) black-white     </option> | ||||
|  | ||||
|             <option value="epd_7_in_5_v2_colour">       7.5" v2 (800x400px) colour          </option> | ||||
|             <option value="epd_7_in_5_v2">              7.5" v2 (800x400px) black-white     </option> | ||||
|  | ||||
|             <option value="epd_7_in_5_colour">          7.5" v1 (600x384px) colour          </option> | ||||
|             <option value="epd_7_in_5">                 7.5" v1 (600x384px) black-white     </option> | ||||
|  | ||||
|             <option value="epd_5_in_83_colour">         5.83" colour                        </option> | ||||
|             <option value="epd_5_in_83">                5.83" black-white                   </option> | ||||
|  | ||||
|             <option value="epd_4_in_2_colour">          4.2" colour                         </option> | ||||
|             <option value="epd_4_in_2">                 4.2" black-white                    </option> | ||||
|         </select> | ||||
|     </div> | ||||
|  | ||||
|  | ||||
|     <!-- Update interval start--> | ||||
|     <div class="col"> | ||||
|         <label>Update interval</label><br> | ||||
|         <select class="form-control" id="update_interval" name="update_interval"> | ||||
|             <option value=60 checked>           every 60 minutes                        </option> | ||||
|             <option value=30>                   every 30 minutes                        </option> | ||||
|             <option value=20>                   every 20 minutes                        </option> | ||||
|             <option value=15>                   every 15 minutes                        </option> | ||||
|             <option value=10>                   every 10 minutes                        </option> | ||||
|         </select> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Update interval end--> | ||||
|  | ||||
|  | ||||
|     <!-- Orientation start --> | ||||
|     <div class="col"> | ||||
|         <label>Orientation</label><br> | ||||
|  | ||||
|         <select class="form-control" id="orientation" name="orientation"> | ||||
|             <option value=0 checked>            Flex cable left                         </option> | ||||
|             <option value=180>                  Flex cable right                        </option> | ||||
|         </select> | ||||
|  | ||||
|     </div> | ||||
|  | ||||
|     </div><br> <!-- row end --> | ||||
|  | ||||
|     <!-- Calibration start --> | ||||
|     <div class="form-group"> | ||||
|         <label>When should the display be calibrated? (Leave blank if you're unsure)</label> | ||||
|  | ||||
|         <!-- Info about calibration (collapsible info)--> | ||||
|         <details> | ||||
|  | ||||
|             <summary>Info about calibration</summary> | ||||
|             <blockquote class="blockquote"> | ||||
|                 Calibration is a way to retain nice colours on ePaper displays. It works by flushing colours a few times on the entire display. | ||||
|                 Please choose 3 hours in 24-hour format (0-24) to specify at which hours calibration should be executed. | ||||
|                 Please also note that it takes around 10-20 minutes to calibrate, so best to choose hours when you won't be looking at Inkycal. | ||||
|             </blockquote> | ||||
|  | ||||
|         </details> | ||||
|  | ||||
|  | ||||
|         <!-- Calibration hours input fields--> | ||||
|         <div class="form-row"> | ||||
|             <div class="col"> | ||||
|                 <input type="number" class="form-control" name="calibration_hour_1" value=0 min=0 max=24> | ||||
|             </div> | ||||
|  | ||||
|             <div class="col"> | ||||
|                 <input type="number" class="form-control" name="calibration_hour_2" value=12 min=0 max=24> | ||||
|                 </div> | ||||
|  | ||||
|             <div class="col"> | ||||
|                 <input type="number" class="form-control" name="calibration_hour_3" value=18 min=0 max=24> | ||||
|             </div> | ||||
|         </div> | ||||
|         <!-- Calibration hours input end--> | ||||
|     </div> | ||||
|     <!-- Calibration end--> | ||||
|  | ||||
|     <!-- Info section --> | ||||
|     <div class="form-group"> | ||||
|         <div class="form-check"> | ||||
|             <input type="checkbox" class="form-check-input" id="info_section" name="info_section"> | ||||
|             <label class="form-check-label" for="info_section">Show info section? (shows time of last display-update)</label> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <h4> Common module settings </h4> | ||||
|     <div class="form-row"> | ||||
|         <!-- language selection- shared by all modules --> | ||||
|         <div class="col"> | ||||
|             <label for="language">Language</label> | ||||
|             <select class="form-control" id="language" name="language"> | ||||
|  | ||||
|                 <option value="en" selected>    English                     </option> | ||||
|                 <option value="de">             German                      </option> | ||||
|                 <option value="ru">             Russian                     </option> | ||||
|                 <option value="it">             Italian                     </option> | ||||
|                 <option value="es">             Spanish                     </option> | ||||
|                 <option value="fr">             French                      </option> | ||||
|                 <option value="el">             Greek                       </option> | ||||
|                 <option value="sv">             Swedish                     </option> | ||||
|                 <option value="nl">             Dutch                       </option> | ||||
|                 <option value="pl">             Polish                      </option> | ||||
|                 <option value="ua">             Ukrainian                   </option> | ||||
|                 <option value="nb">             Norwegian                   </option> | ||||
|                 <option value="vi">             Vietnamese                  </option> | ||||
|                 <option value="zh-tw">          Chinese-Taiwanese           </option> | ||||
|                 <option value="zh">             Chinese                     </option> | ||||
|                 <option value="ja">             Japanese                    </option> | ||||
|                 <option value="ko">             Korean                      </option> | ||||
|  | ||||
|             </select> | ||||
|         </div> | ||||
|  | ||||
|  | ||||
|         <!--fontsize selection - shared by all modules--> | ||||
|         <div class="col"> | ||||
|             <label for="fontsize">Fontsize</label> | ||||
|             <input type="number" class="form-control" name="fontsize" placeholder=12 value=12 min=0 max=30> | ||||
|         </div> | ||||
|  | ||||
|         <!--padding-top-bottom - shared by all modules--> | ||||
|         <div class="col"> | ||||
|             <label for="padding_y">Padding top/bottom (in pixels) </label> | ||||
|             <input type="number" class="form-control" name="padding_y" placeholder=10 value=10 min=0 max=30> | ||||
|         </div> | ||||
|  | ||||
|         <!--padding-left-right - shared by all modules--> | ||||
|         <div class="col"> | ||||
|             <label for="padding_x">Padding right/left (in pixels) </label> | ||||
|             <input type="number" class="form-control" name="padding_x" placeholder=10 value=10 min=0 max=30> | ||||
|         </div> | ||||
|  | ||||
|     </div><br> | ||||
|  | ||||
|  | ||||
|     <!--Create templates for modules with their respective config for later use--> | ||||
|     {% for module in conf %} | ||||
|     <template id={{ module["name"] }} > | ||||
|         <div class="card"><div class="card-header">{{ module["name_str"] }} config</div> | ||||
|             <div class="card-body"> | ||||
|  | ||||
|                 {% if module['requires'] != {} %} | ||||
|                     <h5 class="card-title">Required config</h5> | ||||
|                 {% endif %} | ||||
|  | ||||
|                 {% for key in module["requires"] %} | ||||
|                     {% if 'options' in module["requires"][key] %} | ||||
|                         <label for={{key}}>{{module["requires"][key]["label"]}} *</label> | ||||
|  | ||||
|                         <select class="form-control" id={{key}} name={{ module["name"] }}_{{key}} required> | ||||
|                         {% for option in module["requires"][key]['options'] %} | ||||
|                             <option value={{option}}> {{option}} </option> | ||||
|                         {% endfor %} | ||||
|                         </select> | ||||
|  | ||||
|                         <div class="invalid-feedback">Sorry, but this field should not be empty</div> | ||||
|                         <div class="valid-feedback"> Looks good! </div> | ||||
|                     {% endif %} | ||||
|  | ||||
|                     {% if not 'options' in module["requires"][key] %} | ||||
|                         <label for={{key}}>{{module["requires"][key]["label"]}} *</label> | ||||
|                         <input type="text" class="form-control" id={{key}} name={{ module["name"] }}_{{key}} required> | ||||
|                         <div class="invalid-feedback">Sorry, but this field should not be empty</div> | ||||
|                         <div class="valid-feedback"> Looks good! </div> | ||||
|                     {% endif %} | ||||
|                     <br> | ||||
|                 {% endfor %} | ||||
|  | ||||
|  | ||||
|                 {% if module['optional'] != {} %} | ||||
|                     <h5 class="card-title">Optional config</h5> | ||||
|                 {% endif %} | ||||
|  | ||||
|                 {% for key in module["optional"] %} | ||||
|  | ||||
|                     {% if 'options' in module["optional"][key] %} | ||||
|                         <label for={{key}}>{{module["optional"][key]["label"]}}</label> | ||||
|  | ||||
|                         <select class="form-control" id={{key}} name={{ module["name"] }}_{{key}}> | ||||
|                         {% for option in module["optional"][key]['options'] %} | ||||
|                             <option value={{option}}> {{option}} </option> | ||||
|                         {% endfor %} | ||||
|                         </select> | ||||
|  | ||||
|                         <div class="invalid-feedback">Sorry, but this field should not be empty</div> | ||||
|                         <div class="valid-feedback"> Looks good! </div> | ||||
|                     {% endif %} | ||||
|  | ||||
|                     {% if not 'options' in module["optional"][key] %} | ||||
|                         <label for={{key}}>{{module["optional"][key]["label"]}}</label> | ||||
|                         <input type="text" class="form-control" id={{key}} name={{ module["name"] }}_{{key}}> | ||||
|                     {% endif %} | ||||
|                 {% endfor %} | ||||
|  | ||||
|             </div> | ||||
|         </div> | ||||
|     </template> | ||||
|     {% endfor %} | ||||
|  | ||||
|     <h4> Modules config </h4> | ||||
|  | ||||
|     <div class="alert alert-primary" role="alert">Fields marked with an asterisk(*) are required</div> | ||||
|  | ||||
|     <!-- module 1 selection --> | ||||
|     <div class="form-row"> | ||||
|         <div class="col-md-10"> | ||||
|         <label for="module1">Top section module</label> | ||||
|             <select class="form-control" id="module1" name="module1"> | ||||
|                 <option value="None" checked>Empty</option> | ||||
|                 {% for module in conf%} | ||||
|                     <option value={{ module['name'] }} > {{module['name_str'] }} </option> | ||||
|                 {% endfor %} | ||||
|             </select> | ||||
|         </div> | ||||
|  | ||||
|         <div class="col-md-2"> | ||||
|             <label for="module1_height">Height in percent</label> | ||||
|             <input type="number" class="form-control" name="module1_height" value=10 placeholder=10 min=0 max=100> | ||||
|         </div> | ||||
|  | ||||
|     </div><br> | ||||
|  | ||||
|     <!-- placeholder div --> | ||||
|     <div id="module1_conf"></div> | ||||
|  | ||||
|  | ||||
|     <!-- module 2 selection --> | ||||
|     <div class="form-row"> | ||||
|         <div class="col-md-10"> | ||||
|         <label for="module2">Middle section module</label> | ||||
|             <select class="form-control" id="module2" name="module2"> | ||||
|                 <option value="None" checked>Empty</option> | ||||
|                 {% for module in conf%} | ||||
|                     <option value={{ module['name'] }} > {{module['name_str'] }} </option> | ||||
|                 {% endfor %} | ||||
|             </select> | ||||
|         </div> | ||||
|         <div class="col-md-2"> | ||||
|             <label for="module2_height">Height in percent</label> | ||||
|             <input type="number" class="form-control" name="module2_height" value=65 placeholder=65 min=0 max=100> | ||||
|         </div> | ||||
|     </div><br> | ||||
|  | ||||
|     <!-- placeholder div --> | ||||
|     <div id="module2_conf"></div> | ||||
|  | ||||
|  | ||||
|     <!-- module 3 selection --> | ||||
|     <div class="form-row"> | ||||
|         <div class="col-md-10"> | ||||
|         <label for="module3">Bottom section module</label> | ||||
|             <select class="form-control" id="module3" name="module3"> | ||||
|                 <option value="None" checked>Empty</option> | ||||
|                 {% for module in conf%} | ||||
|                     <option value={{ module['name'] }} > {{module['name_str'] }} </option> | ||||
|                 {% endfor %} | ||||
|             </select> | ||||
|         </div> | ||||
|         <div class="col-md-2"> | ||||
|             <label for="module3_height">Height in percent</label> | ||||
|             <input type="number" class="form-control" name="module3_height" value=25 placeholder=25 min=0 max=100> | ||||
|         </div> | ||||
|     </div><br> | ||||
|  | ||||
|     <!-- placeholder div --> | ||||
|     <div id="module3_conf"></div> | ||||
|  | ||||
|  | ||||
|     <!--Show config of selected modules--> | ||||
|     <script> | ||||
|         $(document).ready(function(){ | ||||
|  | ||||
|             $("#module1").change(function(){ | ||||
|                 $(this).find("option:selected").each(function(){ | ||||
|                     var module1_selection = $(this).attr("value"); | ||||
|                     console.log("Module 1 selected to: "+ module1_selection); | ||||
|                     if(module1_selection != "None"){ | ||||
|  | ||||
|                         // reset module 1 config (avoid showing duplicates) | ||||
|                         $("#module1_conf").replaceWith('<div id="module1_conf"></div>'); | ||||
|  | ||||
|                         // add and render the config for the selected module | ||||
|                         var module1_template = document.querySelector("#"+module1_selection); | ||||
|                         var clone = document.importNode(module1_template.content, true); | ||||
|                         $("#module1_conf").append(clone); | ||||
|  | ||||
|                         // With the selected module name known, we can replace the name tag of that module's config for unique id's | ||||
|                         // This allows having multiple modules running with different configs for each instance | ||||
|                         $("#module1_conf input").each(function(i) { | ||||
|                             //console.log($(this).attr('name', $(this).attr('name').replace(module1_selection, "module1"))); | ||||
|                             $(this).attr('name', $(this).attr('name').replace(module1_selection, "module1")); | ||||
|                         }); | ||||
|                         $("#module1_conf select").each(function(i) { | ||||
|                             //console.log($(this).attr('name', $(this).attr('name').replace(module1_selection, "module1"))); | ||||
|                             $(this).attr('name', $(this).attr('name').replace(module1_selection, "module1")); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         // revert to empty section | ||||
|                         $("#module1_conf").replaceWith('<div id="module1_conf"></div>'); | ||||
|                     } | ||||
|                 }); | ||||
|             }).change(); | ||||
|  | ||||
|             $("#module2").change(function(){ | ||||
|                 $(this).find("option:selected").each(function(){ | ||||
|                     var module2_selection = $(this).attr("value"); | ||||
|                     console.log("Module 2 selected to: "+ module2_selection); | ||||
|                     if(module2_selection != "None"){ | ||||
|  | ||||
|                         // reset module 2 config (avoid showing duplicates) | ||||
|                         $("#module2_conf").replaceWith('<div id="module2_conf"></div>'); | ||||
|  | ||||
|                         // add and render the config for the selected module | ||||
|                         var module2_template = document.querySelector("#"+module2_selection); | ||||
|                         var clone = document.importNode(module2_template.content, true); | ||||
|                         $("#module2_conf").append(clone); | ||||
|  | ||||
|                         // With the selected module name known, we can replace the name tag of that module's config for unique id's | ||||
|                         // This allows having multiple modules running with different configs for each instance | ||||
|                         $("#module2_conf input").each(function(i) { | ||||
|                             //console.log( $(this).attr('name').replace(module2_selection, "module2")); | ||||
|                             $(this).attr('name', $(this).attr('name').replace(module2_selection, "module2")); | ||||
|                         }); | ||||
|                         $("#module2_conf select").each(function(i) { | ||||
|                             //console.log($(this).attr('name', $(this).attr('name').replace(module2_selection, "module2"))); | ||||
|                             $(this).attr('name', $(this).attr('name').replace(module2_selection, "module2")); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         // revert to empty section | ||||
|                         $("#module2_conf").replaceWith('<div id="module2_conf"></div>'); | ||||
|                     } | ||||
|                 }); | ||||
|             }).change(); | ||||
|  | ||||
|             $("#module3").change(function(){ | ||||
|                 $(this).find("option:selected").each(function(){ | ||||
|                     var module3_selection = $(this).attr("value"); | ||||
|                     console.log("Module 3 selected to: "+ module3_selection); | ||||
|                     if(module3_selection != "None"){ | ||||
|  | ||||
|                         // reset module 3 config (avoid showing duplicates) | ||||
|                         $("#module3_conf").replaceWith('<div id="module3_conf"></div>'); | ||||
|  | ||||
|                         // add and render the config for the selected module | ||||
|                         var module3_template = document.querySelector("#"+module3_selection); | ||||
|                         var clone = document.importNode(module3_template.content, true); | ||||
|                         $("#module3_conf").append(clone); | ||||
|  | ||||
|                         // With the selected module name known, we can replace the name tag of that module's config for unique id's | ||||
|                         // This allows having multiple modules running with different configs for each instance | ||||
|                         $("#module3_conf input").each(function(i) { | ||||
|                             //console.log( $(this).attr('name').replace(module3_selection, "module3")); | ||||
|                             $(this).attr('name', $(this).attr('name').replace(module3_selection, "module3")); | ||||
|                         }); | ||||
|                         $("#module3_conf select").each(function(i) { | ||||
|                             //console.log($(this).attr('name', $(this).attr('name').replace(module3_selection, "module3"))); | ||||
|                             $(this).attr('name', $(this).attr('name').replace(module3_selection, "module3")); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         // revert to empty section | ||||
|                         $("#module3_conf").replaceWith('<div id="module3_conf"></div>'); | ||||
|                     } | ||||
|                 }); | ||||
|             }).change(); | ||||
|         }); | ||||
|     </script> | ||||
|  | ||||
|     <script> | ||||
|     (function() { | ||||
|       'use strict'; | ||||
|       window.addEventListener('load', function() { | ||||
|         var forms = document.getElementsByClassName('needs-validation'); | ||||
|         var validation = Array.prototype.filter.call(forms, function(form) { | ||||
|           form.addEventListener('submit', function(event) { | ||||
|             if (form.checkValidity() === false) { | ||||
|               event.preventDefault(); | ||||
|               event.stopPropagation(); | ||||
|             } | ||||
|             form.classList.add('was-validated'); | ||||
|           }, false); | ||||
|         }); | ||||
|       }, false); | ||||
|     })(); | ||||
|     </script> | ||||
|  | ||||
|     <br> | ||||
|     <div class="form-group"> | ||||
|         <button class="btn btn-primary" type="submit">Generate settings file</button> | ||||
|     </div> | ||||
|  | ||||
| </form> | ||||
|  | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										13
									
								
								server/app/templates/wifi.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/app/templates/wifi.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| <!-- Main container --> | ||||
| {% block content %} | ||||
|  | ||||
| <!-- Wrap everything in a container--> | ||||
| <div class="container"> | ||||
|  | ||||
| <!-- heading --> | ||||
| <h3>Raspberry Pi Wifi setup (coming soon)</h3> | ||||
|  | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										4
									
								
								server/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import os | ||||
|  | ||||
| class Config(object): | ||||
|     SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' | ||||
							
								
								
									
										6
									
								
								server/microblog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								server/microblog.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from app import app | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(debug=True, host='0.0.0.0') | ||||
|  | ||||
| # pip3 install flask flask-wtf | ||||
							
								
								
									
										837
									
								
								settings-UI.html
									
									
									
									
									
								
							
							
						
						
									
										837
									
								
								settings-UI.html
									
									
									
									
									
								
							| @@ -1,837 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|  | ||||
| <head> | ||||
|   <title>Settings-File Generator v2.0.0 BETA</title> | ||||
|   <script src="https://code.jquery.com/jquery-3.3.1.min.js" | ||||
|     integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> | ||||
|   <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.css"> | ||||
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/tocas-ui/2.3.3/tocas.js"></script> | ||||
|   <style> | ||||
|     body { | ||||
|       background-color: #eaeaea; | ||||
|     } | ||||
|   </style> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|   <br><br> | ||||
|   <div class="ts container"> | ||||
|     <div class="ts segment"> | ||||
|       <div class="ts header"> | ||||
|         Setting Generator, v.2.0.0 BETA | ||||
|         <div class="sub header"><a href="https://github.com/aceisace/Inky-Calendar">For Inky-Calendar Project of | ||||
|             Ace-Innovation Laboratory (by aceisace)</a><br> | ||||
|           <img src="https://github.com/aceisace/Inky-Calendar/blob/dev_ver2_0/Gallery/logo.png?raw=true" | ||||
|             width="1000" alt="logo"> | ||||
|           <div> | ||||
|           </div> | ||||
|           <ins>If no value is filled in for any of the row, the default value will be used.</ins> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <form class="ts form"> | ||||
|       <blockquote> | ||||
|         <div class="content"> | ||||
|           <p>Instructions<br> | ||||
|             Insert your personal details and preferences and click on 'Generate'. Copy the downloaded file to the | ||||
|             Raspberry Pi. The location does not matter, however, you need to know the path to this file. | ||||
|           </p> | ||||
|         </div> | ||||
|       </blockquote> | ||||
|  | ||||
|       <fieldset> | ||||
|         <legend> | ||||
|           General settings | ||||
|         </legend> | ||||
|         <div class="field"> | ||||
|           <label>On which day does the week start on in your country?</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="week_monday" type="radio" name="hr" checked> | ||||
|               <label for="week_monday">Monday</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="week_sunday" type="radio" name="hr"> | ||||
|               <label for="week_sunday">Sunday</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>At which hours (in 24 hour-format) should the display be calibrated? Leave blank if you're not | ||||
|             sure.</label> | ||||
|           <details class="ts accordion"> | ||||
|             <summary> | ||||
|               <i class="dropdown icon"></i> Info | ||||
|             </summary> | ||||
|             <div class="content"> | ||||
|               <p>Calibration refers to the process of flushing the display with a single colour to prevent 'ghosting' | ||||
|                 (an | ||||
|                 effect specific to E-Paper displays where the remnants of the previous image can be seen on the current | ||||
|                 one). It takes several minutes to finish the calibration(around 10 mins for the 2-colour displays and | ||||
|                 around 20 mins for the 3-colour displays) so please choose hours where you are less likely to need the | ||||
|                 display. It is recommended to calibrate at least thrice a day.</p> | ||||
|             </div> | ||||
|           </details> | ||||
|           <input id="calibration_hours" type="text" placeholder="0,12,18"> | ||||
|         </div> | ||||
|         <div class="field"> | ||||
|           <label>Which E-Paper model are you using?</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="9_in_7" type="radio" name="dp" checked> | ||||
|               <label for="9_in_7">9.7" ePaper</label> | ||||
|             </div> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_7_in_5_v3_colour" type="radio" name="dp" checked> | ||||
|               <label for="epd_7_in_5_v3_colour">7.5" v3 (880x528px) colour (latest)</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_7_in_5_v3" type="radio" name="dp"> | ||||
|               <label for="epd_7_in_5_v3">7.5" v3 (880x528px) black-white (latest)</label> | ||||
|             </div> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_7_in_5_v2_colour" type="radio" name="dp" checked> | ||||
|               <label for="epd_7_in_5_v2_colour">7.5" v2 (800x400px) colour</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_7_in_5_v2" type="radio" name="dp"> | ||||
|               <label for="epd_7_in_5_v2">7.5" v2 (800x400px) black-white</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_7_in_5_colour" type="radio" name="dp"> | ||||
|               <label for="epd_7_in_5_colour">7.5" v1 (600x384px) colour</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_7_in_5" type="radio" name="dp"> | ||||
|               <label for="epd_7_in_5">7.5" v1 (600x384px) black-white</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_5_in_83_colour" type="radio" name="dp"> | ||||
|               <label for="epd_5_in_83_colour">5.83" colour</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_5_in_83" type="radio" name="dp"> | ||||
|               <label for="epd_5_in_83">5.83" black-white</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_4_in_2_colour" type="radio" name="dp"> | ||||
|               <label for="epd_4_in_2_colour">4.2" colour</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="epd_4_in_2" type="radio" name="dp"> | ||||
|               <label for="epd_4_in_2">4.2" black-white</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>How often should the display be refreshed?</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="update_10_mins" type="radio" name="aa"> | ||||
|               <label for="update_10_mins">every 10 minutes. Not recommended for 3-colour E-Papers!</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="update_15_mins" type="radio" name="aa"> | ||||
|               <label for="update_15_mins">every 15 minutes</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="update_20_mins" type="radio" name="aa"> | ||||
|               <label for="update_20_mins">every 20 minutes</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="update_30_mins" type="radio" name="aa"> | ||||
|               <label for="update_30_mins">every 30 minutes</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="update_60_mins" type="radio" name="aa" checked> | ||||
|               <label for="update_60_mins">every 60 minutes (recommended)</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Which language should be used in the software?</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_en" type="radio" name="la" checked> | ||||
|               <label for="language_en">English</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_de" type="radio" name="la"> | ||||
|               <label for="language_de">German</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_ru" type="radio" name="la"> | ||||
|               <label for="language_ru">Russian</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_it" type="radio" name="la"> | ||||
|               <label for="language_it">Italian</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_es" type="radio" name="la"> | ||||
|               <label for="language_es">Spanish</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_fr" type="radio" name="la"> | ||||
|               <label for="language_fr">French</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_el" type="radio" name="la"> | ||||
|               <label for="language_el">Greek</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_sv" type="radio" name="la"> | ||||
|               <label for="language_sv">Swedish</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_nl" type="radio" name="la"> | ||||
|               <label for="language_nl">Dutch</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_pl" type="radio" name="la"> | ||||
|               <label for="language_pl">Polish</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_ua" type="radio" name="la"> | ||||
|               <label for="language_ua">Ukrainian</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_nb" type="radio" name="la"> | ||||
|               <label for="language_nb">Norwegian</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_vi" type="radio" name="la"> | ||||
|               <label for="language_vi">Vietnamese</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_zh_tw" type="radio" name="la"> | ||||
|               <label for="language_zh_tw">Chinese-Taiwanese</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_zh" type="radio" name="la"> | ||||
|               <label for="language_zh">Chinese</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_ja" type="radio" name="la"> | ||||
|               <label for="language_ja">Japanese</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="language_ko" type="radio" name="la"> | ||||
|               <label for="language_ko">Korean</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Which units are used in your country?</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="metric" type="radio" name="un" checked> | ||||
|               <label for="metric">Metric</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="imperial" type="radio" name="un"> | ||||
|               <label for="imperial">Imperial</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Which hour-format do you prefer?</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="24_hours" type="radio" name="tf" checked> | ||||
|               <label for="24_hours">24-hour format</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="12_hours" type="radio" name="tf"> | ||||
|               <label for="12_hours">12-hour format</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Show an info section? The info section will be shown at the very bottom of the display and shows the time of last update.</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="info_yes" type="radio" name="info_section" checked> | ||||
|               <label for="info_yes">Yes, show an info section</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="info_no" type="radio" name="info_section"> | ||||
|               <label for="info_no">Do not show the info section</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>What should be displayed in the top section?</label> | ||||
|           <div class="ts checkboxes" id="cb_top_section"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="Weather" type="radio" name="ts" checked> | ||||
|               <label for="Weather">Weather</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="top_blank" type="radio" name="ts"> | ||||
|               <label for="top_blank">Nothing</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field" id="pnl_top_height"> | ||||
|         <label>Height of the top section</label> | ||||
|         <details class="ts accordion"> | ||||
|           <summary> | ||||
|             <i class="dropdown icon"></i> Info | ||||
|           </summary> | ||||
|           <div class="content"> | ||||
|         <p>Section height is calculated relative to other sections. With this approach you can choose pixel-perfect, relative or percentage panel heights.</p></div> | ||||
|         </details> | ||||
|         <input id="top_height" type="number" min="1" max="100" placeholder="10"> | ||||
|       </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>What should be displayed in the middle (main) section?</label> | ||||
|           <div class="ts checkboxes" id="cb_middle_section"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="Calendar" type="radio" name="ms" checked> | ||||
|               <label for="Calendar">A monthly Calendar</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="Agenda" type="radio" name="ms"> | ||||
|               <label for="Agenda">Agenda of upcoming events</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="Image" type="radio" name="ms"> | ||||
|               <label for="Image">An image</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="middle_blank" type="radio" name="ms"> | ||||
|               <label for="middle_blank">Nothing</label> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|         </div> | ||||
|  | ||||
|         <div class="field" id="pnl_middle_height"> | ||||
|         <label>Height of the middle section</label> | ||||
|         <details class="ts accordion"> | ||||
|           <summary> | ||||
|             <i class="dropdown icon"></i> Info | ||||
|           </summary> | ||||
|           <div class="content"> | ||||
|         <p>Section height is calculated relative to other sections. With this approach you can choose pixel-perfect, relative or percentage panel heights.</p></div> | ||||
|         </details> | ||||
|         <input id="middle_height" type="number" min="1" max="100" placeholder="65"> | ||||
|       </div> | ||||
|  | ||||
|  | ||||
|         <div class="field" id="Image_Config" style="display:none;"> | ||||
|           <div class="field"> | ||||
|             <label>What is the URl or path of the image?</label> | ||||
|             <details class="ts accordion"> | ||||
|               <summary> | ||||
|                 <i class="dropdown icon"></i> Info | ||||
|               </summary> | ||||
|               <div class="content"> | ||||
|                 The following parameters will be substituted: | ||||
|                 <ul> | ||||
|                   <li><code>{model}</code> - substituted by the E-Paper model name.</li> | ||||
|                   <li><code>{width}</code> - substituted by the panel width.</li> | ||||
|                   <li><code>{height}</code> - substituted by the panel width.</li> | ||||
|                 </ul> | ||||
|               </div> | ||||
|             </details> | ||||
|             <input id="image_path" type="text" | ||||
|               placeholder="https://github.com/aceisace/Inky-Calendar/blob/master/Gallery/Inky-Calendar-logo.png?raw=true" /> | ||||
|           </div> | ||||
|  | ||||
|           <div class="field"> | ||||
|             <label>Do you want to send extra data while obtaining the image?</label> | ||||
|             <details class="ts accordion"> | ||||
|               <summary> | ||||
|                 <i class="dropdown icon"></i> Info | ||||
|               </summary> | ||||
|               <div class="content"> | ||||
|                 <p>Optional data. When specified, this data is sent as Json to the image url using POST. | ||||
|                   <br />This is useful for some dynamically generated images. | ||||
|                 </p> | ||||
|               </div> | ||||
|             </details> | ||||
|             <textarea id="image_path_body" type="text" rows="4" placeholder='[ | ||||
|   "https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics" | ||||
| ]'></textarea> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>What should be displayed in the bottom section?</label> | ||||
|           <div class="ts checkboxes" id="cb_bottom_section"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="RSS" type="radio" name="bs" checked> | ||||
|               <label for="RSS">RSS-feeds</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="bottom_blank" type="radio" name="bs"> | ||||
|               <label for="bottom_blank">Nothing</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field" id="pnl_bottom_height"> | ||||
|         <label>Height of the bottom section</label> | ||||
|         <details class="ts accordion"> | ||||
|           <summary> | ||||
|             <i class="dropdown icon"></i> Info | ||||
|           </summary> | ||||
|           <div class="content"> | ||||
|         <p>Section height is calculated relative to other sections. With this approach you can choose pixel-perfect, relative or percentage panel heights.</p></div> | ||||
|         </details> | ||||
|         <input id="bottom_height" type="number" min="1" max="100" placeholder="25"> | ||||
|       </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Display orientation</label> | ||||
|           <div class="ts checkboxes"> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="DisplayNotRotated" type="radio" name="bbs" checked> | ||||
|               <label for="DisplayNotRotated">Normal</label> | ||||
|             </div> | ||||
|             <div class="ts radio checkbox"> | ||||
|               <input id="DisplayRotated" type="radio" name="bbs"> | ||||
|               <label for="DisplayRotated">Upside-down</label> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|       </fieldset> | ||||
|  | ||||
|       <fieldset> | ||||
|         <legend> | ||||
|           Panel-specific settings | ||||
|         </legend> | ||||
|         <div class="field"> | ||||
|           <label>iCalendar URL/s, separated by comma: url1, url2, url3</label> | ||||
|           <input id="ical_urls" type="text" | ||||
|             placeholder="https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics"> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>RSS-Feed URL/s, separated by comma: url1, url2, url3</label> | ||||
|           <input id="rss_urls" type="text" placeholder="http://feeds.bbci.co.uk/news/world/rss.xml#"> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Openweathermap API Key</label> | ||||
|           <details class="ts accordion"> | ||||
|             <summary> | ||||
|               <i class="dropdown icon"></i> Info | ||||
|             </summary> | ||||
|             <div class="content"> | ||||
|               <p> Please insert your own Openweathermap API-key to fetch the latest weather info. To find out how to | ||||
|                 create your own key, please click here: <a | ||||
|                   href="https://github.com/aceisace/Inky-Calendar/wiki/Openweathermap-API">Creating an openweathermap | ||||
|                   api-key</a>. If you don't add an api-key, the top section will not show any weather info</p> | ||||
|             </div> | ||||
|           </details> | ||||
|           <input id="api_key" type="text" placeholder=""> | ||||
|         </div> | ||||
|  | ||||
|         <div class="field"> | ||||
|           <label>Location (for weather data)</label> | ||||
|           <details class="ts accordion"> | ||||
|             <summary> | ||||
|               <i class="dropdown icon"></i> Info | ||||
|             </summary> | ||||
|             <div class="content"> | ||||
|               <p>Location refers to the closest weather station from your place. It isn't necessarily the place you live | ||||
|                 in. To find this location, type your city name in the search box on <a | ||||
|                   href="https://openweathermap.org/">openweathermap</a>. The output should be in the following format: | ||||
|                 City Name, Country ISO-Code. Not sure what your ISO code is? Check here: <a | ||||
|                   href="https://countrycode.org/">(find iso-code)</a></p> | ||||
|             </div> | ||||
|           </details> | ||||
|           <input id="location" type="text" placeholder="Stuttgart, DE"> | ||||
|         </div> | ||||
|       </fieldset> | ||||
|  | ||||
|  | ||||
|     </form> | ||||
|     <br> | ||||
|     <button class="ts primary button" onClick="generate()">Generate</button> | ||||
|     <br><br> | ||||
|     <kbd>Developed by Toby Chui for Inkycal Project, modified by aceisace. Licensed under MIT</kbd> | ||||
|     <details class="ts accordion"> | ||||
|       <summary> | ||||
|         <i class="dropdown icon"></i> MIT License | ||||
|       </summary> | ||||
|       <div class="content"> | ||||
|         <p>Copyright 2019-2020 Toby Chui <br> | ||||
|  | ||||
|           Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | ||||
|           documentation files (the "Software"), to deal in the Software without restriction, including without | ||||
|           limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the | ||||
|           Software, and to permit persons to whom the Software is furnished to do so, subject to the following | ||||
|           conditions: | ||||
|  | ||||
|           The above copyright notice and this permission notice shall be included in all copies or substantial portions | ||||
|           of the Software. | ||||
|  | ||||
|           THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED | ||||
|           TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | ||||
|           THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF | ||||
|           CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||||
|           DEALINGS IN THE SOFTWARE.</p> | ||||
|       </div> | ||||
|     </details> | ||||
|   </div> | ||||
|   <br> | ||||
|   <br> | ||||
|  | ||||
|   <script> | ||||
|     $('#cb_middle_section').change(function () { | ||||
|       if ($('#Image').prop("checked")) { | ||||
|         $('#Image_Config').show(); | ||||
|       } else { | ||||
|         $('#Image_Config').hide(); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     function generate() { | ||||
|       var ical_urls = $("#ical_urls").val().trim().split(' ').join('').split(','); | ||||
|       if (ical_urls == "") { | ||||
|         ical_urls = $("#ical_urls").attr("placeholder").split(' ').join('').split(','); | ||||
|       } | ||||
|  | ||||
|       var rss_urls = $("#rss_urls").val().trim().split(' ').join('').split(','); | ||||
|       if (rss_urls == "") { | ||||
|         rss_urls = $("#rss_urls").attr("placeholder").split(' ').join('').split(','); | ||||
|       } | ||||
|  | ||||
|       var update_interval = "60"; | ||||
|       if ($('#update_10_mins').is(':checked')) { | ||||
|         update_interval = "10"; | ||||
|       } | ||||
|       if ($('#update_15_mins').is(':checked')) { | ||||
|         update_interval = "15"; | ||||
|       } | ||||
|       if ($('#update_20_mins').is(':checked')) { | ||||
|         update_interval = "20"; | ||||
|       } | ||||
|       if ($('#update_30_mins').is(':checked')) { | ||||
|         update_interval = "30"; | ||||
|       } | ||||
|       if ($('#update_60_mins').is(':checked')) { | ||||
|         update_interval = "60"; | ||||
|       } | ||||
|  | ||||
|       var api_key = $("#api_key").val().trim(); | ||||
|       if (api_key == "") { | ||||
|         api_key = ""; | ||||
|       } | ||||
|  | ||||
|       var location = $("#location").val().trim(); | ||||
|       if (location == "") { | ||||
|         location = $("#location").attr("placeholder"); | ||||
|       } | ||||
|  | ||||
|       var week_starts_on = "Monday"; | ||||
|       if ($('#week_sunday').is(':checked')) { | ||||
|         week_starts_on = "Sunday"; | ||||
|       } | ||||
|  | ||||
|       var calibration_hours = $("#calibration_hours").val().trim(); | ||||
|       if (calibration_hours == "") { | ||||
|         calibration_hours = $("#calibration_hours").attr("placeholder"); | ||||
|       } | ||||
|  | ||||
|       var model = "epd_7_in_5_v2"; | ||||
|       if ($('#9_in_7').is(':checked')) { | ||||
|         model = "9_in_7"; | ||||
|       } | ||||
|       if ($('#epd_7_in_5_v3_colour').is(':checked')) { | ||||
|         model = "epd_7_in_5_v3_colour"; | ||||
|       } | ||||
|       if ($('#epd_7_in_5_v3').is(':checked')) { | ||||
|         model = "epd_7_in_5_v3"; | ||||
|       } | ||||
|       if ($('#epd_7_in_5_v2_colour').is(':checked')) { | ||||
|         model = "epd_7_in_5_v2_colour"; | ||||
|       } | ||||
|       if ($('#epd_7_in_5_colour').is(':checked')) { | ||||
|         model = "epd_7_in_5_colour"; | ||||
|       } | ||||
|       if ($('#epd_7_in_5').is(':checked')) { | ||||
|         model = "epd_7_in_5"; | ||||
|       } | ||||
|       if ($('#epd_5_in_83_colour').is(':checked')) { | ||||
|         model = "epd_5_in_83_colour"; | ||||
|       } | ||||
|       if ($('#epd_5_in_83').is(':checked')) { | ||||
|         model = "epd_5_in_83"; | ||||
|       } | ||||
|       if ($('#epd_4_in_2_colour').is(':checked')) { | ||||
|         model = "epd_4_in_2_colour"; | ||||
|       } | ||||
|       if ($('#epd_4_in_2').is(':checked')) { | ||||
|         model = "epd_4_in_2"; | ||||
|       } | ||||
|  | ||||
|       var language = "en"; | ||||
|       if ($('#language_de').is(':checked')) { | ||||
|         language = "de"; | ||||
|       } | ||||
|       if ($('#language_ru').is(':checked')) { | ||||
|         language = "ru"; | ||||
|       } | ||||
|       if ($('#language_it').is(':checked')) { | ||||
|         language = "it"; | ||||
|       } | ||||
|       if ($('#language_es').is(':checked')) { | ||||
|         language = "es"; | ||||
|       } | ||||
|       if ($('#language_fr').is(':checked')) { | ||||
|         language = "fr"; | ||||
|       } | ||||
|       if ($('#language_el').is(':checked')) { | ||||
|         language = "el"; | ||||
|       } | ||||
|       if ($('#language_sv').is(':checked')) { | ||||
|         language = "sv"; | ||||
|       } | ||||
|       if ($('#language_nl').is(':checked')) { | ||||
|         language = "nl"; | ||||
|       } | ||||
|       if ($('#language_pl').is(':checked')) { | ||||
|         language = "pl"; | ||||
|       } | ||||
|       if ($('#language_ua').is(':checked')) { | ||||
|         language = "ua"; | ||||
|       } | ||||
|       if ($('#language_nb').is(':checked')) { | ||||
|         language = "nb"; | ||||
|       } | ||||
|       if ($('#language_vi').is(':checked')) { | ||||
|         language = "vi"; | ||||
|       } | ||||
|       if ($('#language_zh_tw').is(':checked')) { | ||||
|         language = "zh_tw"; | ||||
|       } | ||||
|       if ($('#language_zh').is(':checked')) { | ||||
|         language = "zh"; | ||||
|       } | ||||
|       if ($('#language_ja').is(':checked')) { | ||||
|         language = "ja"; | ||||
|       } | ||||
|       if ($('#language_ko').is(':checked')) { | ||||
|         language = "ko"; | ||||
|       } | ||||
|  | ||||
|       var units = "metric"; | ||||
|       if ($('#imperial').is(':checked')) { | ||||
|         units = "imperial"; | ||||
|       } | ||||
|  | ||||
|  | ||||
|       var info_section = true; | ||||
|       if ($('#info_no').is(':checked')) { | ||||
|         info_section = false; | ||||
|       } | ||||
|  | ||||
|       var hours = 24; | ||||
|       if ($('#12_hours').is(':checked')) { | ||||
|         hours = 12; | ||||
|       } | ||||
|  | ||||
|       var top_section = "Weather"; | ||||
|       if ($('#top_blank').is(':checked')) { | ||||
|         top_section = ""; | ||||
|       } | ||||
|  | ||||
|       var middle_section = "Calendar"; | ||||
|       if ($('#Agenda').is(':checked')) { | ||||
|         middle_section = "Agenda"; | ||||
|       } | ||||
|       if ($('#Image').is(':checked')) { | ||||
|         middle_section = "Image"; | ||||
|       } | ||||
|       if ($('#middle_blank').is(':checked')) { | ||||
|         middle_section = ""; | ||||
|       } | ||||
|  | ||||
|       var bottom_section = "RSS"; | ||||
|       if ($('#bottom_blank').is(':checked')) { | ||||
|         bottom_section = ""; | ||||
|       } | ||||
|  | ||||
|       top_section_height    = $("#top_height").val().trim() | ||||
|       top_section_height = top_section_height=="" ? null : Number(top_section_height) | ||||
|  | ||||
|       middle_section_height = $("#middle_height").val().trim() | ||||
|       middle_section_height = middle_section_height=="" ? null : Number(middle_section_height) | ||||
|  | ||||
|       bottom_section_height = $("#bottom_height").val().trim() | ||||
|       bottom_section_height = bottom_section_height=="" ? null : Number(bottom_section_height) | ||||
|  | ||||
|       var display_orientation = "normal"; | ||||
|       if ($('#DisplayRotated').is(':checked')) { | ||||
|         display_orientation = "upside_down"; | ||||
|       } | ||||
|  | ||||
|       var image_path = $("#image_path").val().trim(); | ||||
|       if (image_path == "") { | ||||
|         image_path = $("#image_path").attr("placeholder"); | ||||
|       } | ||||
|  | ||||
|       var image_path_body = $("#image_path").val().trim(); | ||||
|  | ||||
|       //console.log(ical_urls, rss_urls, update_interval, api_key, location, week_starts_on, calibration_hours, model, language, units, hours, top_section, middle_section, bottom_section); | ||||
|       downloadSettingsAsJson( | ||||
|         ical_urls,  | ||||
|         rss_urls,  | ||||
|         update_interval,  | ||||
|         api_key, location,  | ||||
|         week_starts_on,  | ||||
|         calibration_hours,  | ||||
|         model, language,  | ||||
|         units, hours, | ||||
|         info_section,  | ||||
|         top_section, | ||||
|         top_section_height, | ||||
|         middle_section, | ||||
|         middle_section_height, | ||||
|         bottom_section, | ||||
|         bottom_section_height, | ||||
|         display_orientation,  | ||||
|         image_path,  | ||||
|         image_path_body) | ||||
|     } | ||||
|  | ||||
|     function TrimSingleQuotes(text) { | ||||
|       return text.replace(/^'+/g, "").replace(/'+$/g, "") | ||||
|     } | ||||
|  | ||||
|     function downloadSettingsAsJson( | ||||
|       ical_urls, | ||||
|       rss_urls, | ||||
|       update_interval, | ||||
|       api_key, | ||||
|       location, | ||||
|       week_starts_on, | ||||
|       calibration_hours, | ||||
|       model, | ||||
|       language, | ||||
|       units, | ||||
|       hours, | ||||
|       info_section, | ||||
|       top_section, | ||||
|       top_section_height, | ||||
|       middle_section, | ||||
|       middle_section_height, | ||||
|       bottom_section, | ||||
|       bottom_section_height, | ||||
|       display_orientation, | ||||
|       image_path, | ||||
|       image_path_body | ||||
|     ) { | ||||
|       var result = { | ||||
|         "language": language,         // "en", "de", "fr", "jp" etc. | ||||
|         "units": units,               // "metric", "imperial" | ||||
|         "hours": Number(hours),       // 24, 12 | ||||
|         "model": model, | ||||
|         "update_interval": Number(update_interval),       // 10, 15, 20, 30, 60 | ||||
|         "calibration_hours": calibration_hours.split(",").map(function (x) { return Number(x); }),              // Do not change unless you know what you are doing | ||||
|         "display_orientation": display_orientation, | ||||
|         "info_section":info_section, | ||||
|         "panels": [] | ||||
|       }; | ||||
|  | ||||
|       switch (top_section) { | ||||
|         case "Weather": | ||||
|           result.panels.push( | ||||
|             { | ||||
|               "location": "top", | ||||
|               "type": "Weather", | ||||
|               "height"    : top_section_height, | ||||
|               "config": { | ||||
|                 "api_key": api_key,     //Your openweathermap API-KEY -> "api-key" | ||||
|                 "location": location      //"City name, Country code" | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|           break; | ||||
|         default: | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       switch (middle_section) { | ||||
|         case "Agenda": | ||||
|         case "Calendar": | ||||
|           result.panels.push( | ||||
|             { | ||||
|               "location": "middle", | ||||
|               "type": middle_section, | ||||
|               "height"    : middle_section_height, | ||||
|               "config": { | ||||
|                 "week_starts_on": week_starts_on,    //"Sunday", "Monday"... | ||||
|                 "ical_urls": ical_urls | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|           break; | ||||
|         case "Image": | ||||
|           result.panels.push( | ||||
|             { | ||||
|               "location": "middle", | ||||
|               "type": middle_section, | ||||
|               "height"    : middle_section_height, | ||||
|               "config": { | ||||
|                 "image_path": TrimSingleQuotes(image_path), | ||||
|                 "image_path_body": image_path_body | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|           break; | ||||
|         default: | ||||
|           break; | ||||
|       } | ||||
|  | ||||
|       switch (bottom_section) { | ||||
|         case "RSS": | ||||
|           result.panels.push( | ||||
|             { | ||||
|               "location": "bottom", | ||||
|               "type": bottom_section, | ||||
|               "height"    : bottom_section_height, | ||||
|               "config": { | ||||
|                 "rss_urls": rss_urls | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|           break; | ||||
|         default: | ||||
|           break; | ||||
|       } | ||||
|       var config = new Blob([JSON.stringify(result, null, "\t")], { type: "text/json" }); | ||||
|       var link = document.createElement('link'); | ||||
|       link.href = window.URL.createObjectURL(config); | ||||
|       var a = document.createElement('A'); | ||||
|       a.href = link.href; | ||||
|       a.download = link.href.substr(link.href.lastIndexOf('/') + 1); | ||||
|       document.body.appendChild(a); | ||||
|       $(a).attr('download', 'settings.json'); | ||||
|       a.click(); | ||||
|       document.body.removeChild(a); | ||||
|     } | ||||
|  | ||||
|   </script> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user