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 | # Settings and Layout | ||||||
| from inkycal.config.layout import Layout | #from inkycal.config.layout import Layout | ||||||
| from inkycal.config.settings_parser import Settings | #from inkycal.config.settings_parser import Settings | ||||||
|  | from inkycal.display import Display | ||||||
|  |  | ||||||
| # All supported inkycal_modules | # All supported inkycal_modules | ||||||
| import inkycal.modules.inkycal_agenda | import inkycal.modules.inkycal_agenda | ||||||
| import inkycal.modules.inkycal_calendar | import inkycal.modules.inkycal_calendar | ||||||
| import inkycal.modules.inkycal_weather | import inkycal.modules.inkycal_weather | ||||||
| import inkycal.modules.inkycal_rss | import inkycal.modules.inkycal_rss | ||||||
| # import inkycal.modules.inkycal_image | #import inkycal.modules.inkycal_image | ||||||
| # import inkycal.modules.inkycal_server | # import inkycal.modules.inkycal_server | ||||||
|  |  | ||||||
| # Main file | # Main file | ||||||
| from inkycal.main import Inkycal | 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): |     if (model != None) and (width == None) and (height == None): | ||||||
|       display_dimensions = { |       display_dimensions = { | ||||||
|         '9_in_7': (1200, 825), |         '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_colour': (800, 480), | ||||||
|         'epd_7_in_5_v2': (800, 480), |         'epd_7_in_5_v2': (800, 480), | ||||||
|         'epd_7_in_5_colour': (640, 384), |         'epd_7_in_5_colour': (640, 384), | ||||||
| @@ -116,6 +114,14 @@ class Layout: | |||||||
|         size = (self.bottom_section_width, self.bottom_section_height) |         size = (self.bottom_section_width, self.bottom_section_height) | ||||||
|       return size |       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__': | if __name__ == '__main__': | ||||||
|   print('running {0} in standalone/debug mode'.format( |   print('running {0} in standalone/debug mode'.format( | ||||||
|     os.path.basename(__file__).split('.py')[0])) |     os.path.basename(__file__).split('.py')[0])) | ||||||
|   | |||||||
| @@ -23,7 +23,6 @@ class Settings: | |||||||
|   _supported_update_interval = [10, 15, 20, 30, 60] |   _supported_update_interval = [10, 15, 20, 30, 60] | ||||||
|   _supported_display_orientation = ['normal', 'upside_down'] |   _supported_display_orientation = ['normal', 'upside_down'] | ||||||
|   _supported_models = [ |   _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_v2_colour', 'epd_7_in_5_v2', | ||||||
|   'epd_7_in_5_colour', 'epd_7_in_5', |   'epd_7_in_5_colour', 'epd_7_in_5', | ||||||
|   'epd_5_in_83_colour','epd_5_in_83', |   '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): | def write(image, xy, box_size, text, font=None, **kwargs): | ||||||
|   """Write text on specified image |   """Write text on specified image | ||||||
|   image = on which image should the text be added? |   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) |   box_size = size of text-box -> (width,height) | ||||||
|   text = string (what to write) |   text = string (what to write) | ||||||
|   font = which font to use |   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 | #!/usr/bin/python3 | ||||||
| from inkycal.custom import * | # -*- 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 os | ||||||
| import traceback | import traceback | ||||||
| import logging | import logging | ||||||
| import arrow | import arrow | ||||||
| import time | import time | ||||||
|  | import json | ||||||
|  |  | ||||||
| try: | try: | ||||||
|   from PIL import Image |   from PIL import Image | ||||||
| @@ -20,36 +27,36 @@ except ImportError: | |||||||
|   print('numpy is not installed! Please install with:') |   print('numpy is not installed! Please install with:') | ||||||
|   print('pip3 install numpy') |   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) | logger.setLevel(level=logging.ERROR) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Inkycal: | class Inkycal: | ||||||
|   """Inkycal main class""" |   """Inkycal main class""" | ||||||
|  |  | ||||||
|   def __init__(self, settings_path, render=True): |   def __init__(self, settings_path, render=True): | ||||||
|     """initialise class |     """Initialise Inkycal | ||||||
|     settings_path = str -> location/folder of settings file |     settings_path = str -> location/folder of settings file | ||||||
|     render = bool -> show something on the ePaper? |     render = bool -> show something on the ePaper? | ||||||
|     """ |     """ | ||||||
|     self._release = '2.0.0beta' |     self._release = '2.0.0' | ||||||
|  |  | ||||||
|     # Check if render is boolean |     # Check if render was set correctly | ||||||
|     if not isinstance(render, bool): |     if render not in [True, False]: | ||||||
|       raise Exception('render must be True or False, not "{}"'.format(render)) |       raise Exception('render must be True or False, not "{}"'.format(render)) | ||||||
|     self.render = render |     self.render = render | ||||||
|  |  | ||||||
|     # Init settings class |     # load settings file - throw an error if file could not be found | ||||||
|     self.Settings = Settings(settings_path) |     try: | ||||||
|  |       with open(settings_path) as file: | ||||||
|  |         settings = json.load(file) | ||||||
|  |         self.settings = settings | ||||||
|  |         #print(self.settings) | ||||||
|  |  | ||||||
|     # Check if display support colour |     except FileNotFoundError: | ||||||
|     self.supports_colour = self.Settings.Layout.supports_colour |       print('No settings file found in specified location') | ||||||
|  |       print('Please double check your path') | ||||||
|     # 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 |     # Option to use epaper image optimisation | ||||||
|     self.optimize = True |     self.optimize = True | ||||||
| @@ -57,27 +64,26 @@ class Inkycal: | |||||||
|     # Load drivers if image should be rendered |     # Load drivers if image should be rendered | ||||||
|     if self.render == True: |     if self.render == True: | ||||||
|  |  | ||||||
|       # Get model and check if colour can be rendered |       # Init Display class with model in settings file | ||||||
|       model= self.Settings.model |  | ||||||
|  |  | ||||||
|       # Init Display class |  | ||||||
|       from inkycal.display import Display |       from inkycal.display import Display | ||||||
|       self.Display = Display(model) |       self.Display = Display(settings["model"]) | ||||||
|  |  | ||||||
|       # get calibration hours |       # check if colours can be rendered | ||||||
|       self._calibration_hours = self.Settings.calibration_hours |       self.supports_colour = True if 'colour' in settings['model'] else False | ||||||
|  |  | ||||||
|       # set a check for calibration |       # init calibration state | ||||||
|       self._calibration_state = False |       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: |       try: | ||||||
|         loader = 'from inkycal.modules import {0}'.format(module) |         loader = f'from inkycal.modules import {module["name"]}' | ||||||
|         module_data = self.Settings.get_config(module) |         print(loader) | ||||||
|         size, conf = module_data['size'], module_data['config'] |         conf = module["config"] | ||||||
|         setup = 'self.{} = {}(size, conf)'.format(module, module) |         #size, conf = module_data['size'], module_data['config'] | ||||||
|  |         setup = f'self.{module} = {module}(size, conf)' | ||||||
|         exec(loader) |         exec(loader) | ||||||
|         exec(setup) |         exec(setup) | ||||||
|         logger.debug(('{}: size: {}, config: {}'.format(module, size, conf))) |         logger.debug(('{}: size: {}, config: {}'.format(module, size, conf))) | ||||||
| @@ -88,6 +94,9 @@ class Inkycal: | |||||||
|           'Could not find module: "{}". Please try to import manually.'.format( |           'Could not find module: "{}". Please try to import manually.'.format( | ||||||
|           module)) |           module)) | ||||||
|  |  | ||||||
|  |       except Exception as e: | ||||||
|  |         print(str(e)) | ||||||
|  |  | ||||||
|     # Give an OK message |     # Give an OK message | ||||||
|     print('loaded inkycal') |     print('loaded inkycal') | ||||||
|  |  | ||||||
| @@ -99,7 +108,7 @@ class Inkycal: | |||||||
|  |  | ||||||
|     # Check if empty, if empty, use value from settings file |     # Check if empty, if empty, use value from settings file | ||||||
|     if interval_mins == None: |     if interval_mins == None: | ||||||
|       interval_mins = self.Settings.update_interval |       interval_mins = self.settings.update_interval | ||||||
|  |  | ||||||
|     # Check if integer |     # Check if integer | ||||||
|     if not isinstance(interval_mins, int): |     if not isinstance(interval_mins, int): | ||||||
| @@ -127,348 +136,18 @@ class Inkycal: | |||||||
|     # Return seconds until next update |     # Return seconds until next update | ||||||
|     return remaining_time |     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: | if __name__ == '__main__': | ||||||
|       print('Generating images for all modules...') |   print('running {0} in standalone/debug mode'.format(filename)) | ||||||
|       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)) |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -212,3 +212,10 @@ class iCalendar: | |||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|   print('running {0} in standalone mode'.format(filename)) |   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 = logging.getLogger(filename) | ||||||
| logger.setLevel(level=logging.ERROR) | logger.setLevel(level=logging.ERROR) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Agenda(inkycal_module): | class Agenda(inkycal_module): | ||||||
|   """Agenda class |   """Agenda class | ||||||
|   Create agenda and show events from given icalendars |   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): |   def __init__(self, section_size, section_config): | ||||||
|     """Initialize inkycal_agenda module""" |     """Initialize inkycal_agenda module""" | ||||||
|  |  | ||||||
|     super().__init__(section_size, section_config) |     super().__init__(section_size, section_config) | ||||||
|     # Module specific parameters |  | ||||||
|     required = ['week_starts_on', 'ical_urls'] |     for param in self.equires: | ||||||
|     for param in required: |  | ||||||
|       if not param in section_config: |       if not param in section_config: | ||||||
|         raise Exception('config is missing {}'.format(param)) |         raise Exception('config is missing {}'.format(param)) | ||||||
|  |  | ||||||
|     # class name |  | ||||||
|     self.name = self.__class__.__name__ |  | ||||||
|  |  | ||||||
|     # module specific parameters |     # module specific parameters | ||||||
|     self.date_format = 'ddd D MMM' |     self.date_format = self.config['date_format'] | ||||||
|     self.time_format = "HH:mm" |     self.time_format = self.config['time_format'] | ||||||
|     self.language = self.config['language'] |     self.language = self.config['language'] | ||||||
|     self.timezone = get_system_tz() |  | ||||||
|     self.ical_urls = self.config['ical_urls'] |     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 |     # give an OK message | ||||||
|     print('{0} loaded'.format(self.name)) |     print('{0} loaded'.format(filename)) | ||||||
|  |  | ||||||
|   def _validate(self): |   def _validate(self): | ||||||
|     """Validate module-specific parameters""" |     """Validate module-specific parameters""" | ||||||
| @@ -191,7 +216,6 @@ class Agenda(inkycal_module): | |||||||
|  |  | ||||||
|     # If no events were found, write only dates and lines |     # If no events were found, write only dates and lines | ||||||
|     else: |     else: | ||||||
|       line_pos = [(0, int(line * line_height)) for line in range(max_lines)] |  | ||||||
|       cursor = 0 |       cursor = 0 | ||||||
|       for _ in agenda_events: |       for _ in agenda_events: | ||||||
|         title = _['title'] |         title = _['title'] | ||||||
| @@ -206,9 +230,8 @@ class Agenda(inkycal_module): | |||||||
|  |  | ||||||
|       logger.info('no events found') |       logger.info('no events found') | ||||||
|  |  | ||||||
|     # Save image of black and colour channel in image-folder |     # return the images ready for the display | ||||||
|     im_black.save(images+self.name+'.png') |     return im_black, im_colour | ||||||
|     im_colour.save(images+self.name+'_colour.png') |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|   print('running {0} in standalone mode'.format(filename)) |   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 |   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): |   def __init__(self, section_size, section_config): | ||||||
|     """Initialize inkycal_calendar module""" |     """Initialize inkycal_calendar module""" | ||||||
|  |  | ||||||
|     super().__init__(section_size, section_config) |     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 |     # module specific parameters | ||||||
|     self.num_font = ImageFont.truetype( |     self.num_font = ImageFont.truetype( | ||||||
|       fonts['NotoSans-SemiCondensed'], size = self.fontsize) |       fonts['NotoSans-SemiCondensed'], size = self.fontsize) | ||||||
|     self.weekstart = self.config['week_starts_on'] |     self.weekstart = self.config['week_starts_on'] | ||||||
|     self.show_events = True |     self.show_events = self.config['show_events'] | ||||||
|     self.date_format = 'D MMM' |     self.date_format = self.config["date_format"] | ||||||
|     self.time_format = "HH:mm" |     self.time_format = self.config['time_format'] | ||||||
|     self.language = self.config['language'] |     self.language = self.config['language'] | ||||||
|  |  | ||||||
|     self.timezone = get_system_tz() |     self.timezone = get_system_tz() | ||||||
|     self.ical_urls = self.config['ical_urls'] |     self.ical_urls = self.config['ical_urls'] | ||||||
|     self.ical_files = [] |     self.ical_files = self.config['ical_files'] | ||||||
|  |  | ||||||
|     # give an OK message |     # give an OK message | ||||||
|     print('{0} loaded'.format(self.name)) |     print('{0} loaded'.format(filename)) | ||||||
|  |  | ||||||
|   def generate_image(self): |   def generate_image(self): | ||||||
|     """Generate image for this module""" |     """Generate image for this module""" | ||||||
|  |  | ||||||
|     # Define new image size with respect to padding |     # Define new image size with respect to padding | ||||||
|     im_width = int(self.width - (self.width * 2 * self.margin_x)) |     im_width = int(self.width - (2 * self.padding_x)) | ||||||
|     im_height = int(self.height - (self.height * 2 * self.margin_y)) |     im_height = int(self.height - (2 * self.padding_y)) | ||||||
|     im_size = im_width, im_height |     im_size = im_width, im_height | ||||||
|  |  | ||||||
|     logger.info('Image size: {0}'.format(im_size)) |     logger.info('Image size: {0}'.format(im_size)) | ||||||
| @@ -80,15 +112,7 @@ class Calendar(inkycal_module): | |||||||
|         im_width, calendar_height)) |         im_width, calendar_height)) | ||||||
|  |  | ||||||
|     # Create grid and calculate icon sizes |     # Create grid and calculate icon sizes | ||||||
|     now = arrow.now(tz = self.timezone) |     calendar_rows, calendar_cols = 6, 7 | ||||||
|     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_width = im_width // calendar_cols | ||||||
|     icon_height = calendar_height // calendar_rows |     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 |     weekday_pos = [(grid_start_x + icon_width*_, month_name_height) for _ in | ||||||
|                    range(calendar_cols)] |                    range(calendar_cols)] | ||||||
|  |  | ||||||
|  |     now = arrow.now(tz = self.timezone) | ||||||
|  |  | ||||||
|     # Set weekstart of calendar to specified weekstart |     # Set weekstart of calendar to specified weekstart | ||||||
|     if self.weekstart == "Monday": |     if self.weekstart == "Monday": | ||||||
|       cal.setfirstweekday(cal.MONDAY) |       cal.setfirstweekday(cal.MONDAY) | ||||||
| @@ -283,9 +309,8 @@ class Calendar(inkycal_module): | |||||||
|               (im_width, self.font.getsize(symbol)[1]), symbol, |               (im_width, self.font.getsize(symbol)[1]), symbol, | ||||||
|               font = self.font) |               font = self.font) | ||||||
|  |  | ||||||
|     # Save image of black and colour channel in image-folder |     # return the images ready for the display | ||||||
|     im_black.save(images+self.name+'.png') |     return im_black, im_colour | ||||||
|     im_colour.save(images+self.name+'_colour.png') |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|   print('running {0} in standalone mode'.format(filename)) |   print('running {0} in standalone mode'.format(filename)) | ||||||
|   | |||||||
| @@ -1,305 +1,32 @@ | |||||||
| #!/usr/bin/python3 | #!/usr/bin/python3 | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
| """ | """ | ||||||
| Image module for Inkycal Project | Image module for inkycal Project | ||||||
| Copyright by aceisace | Copyright by aceisace | ||||||
|  | Development satge: Beta | ||||||
| """ | """ | ||||||
|  |  | ||||||
| from inkycal.modules.template import inkycal_module | from os import path | ||||||
| from inkycal.custom import * |  | ||||||
|  |  | ||||||
| from PIL import ImageOps | from PIL import ImageOps | ||||||
| import requests | import requests | ||||||
| import numpy | import numpy | ||||||
|  |  | ||||||
| filename = os.path.basename(__file__).split('.py')[0] | """----------------------------------------------------------------""" | ||||||
| logger = logging.getLogger(filename) | #path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' | ||||||
| logger.setLevel(level=logging.DEBUG) | #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): | # First determine dimensions | ||||||
|   """Image class | if mode == 'horizontal': | ||||||
|   display an image from a given path or URL |   display_width, display_height == display_height, display_width | ||||||
|   """ |  | ||||||
|   _allowed_layout = ['fill', 'center', 'fit', 'auto'] |  | ||||||
|   _allowed_rotation = [0, 90, 180, 270, 360, 'auto'] |  | ||||||
|   _allowed_colours = ['bw', 'bwr', 'bwy'] |  | ||||||
|  |  | ||||||
|   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 -*- | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
| """ | """ | ||||||
| RSS module for Inky-Calendar Project | RSS module for inkyCal Project | ||||||
| Copyright by aceisace | Copyright by aceisace | ||||||
| """ | """ | ||||||
|  |  | ||||||
| @@ -25,28 +25,48 @@ class RSS(inkycal_module): | |||||||
|   parses rss/atom feeds from given urls |   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): |   def __init__(self, section_size, section_config): | ||||||
|     """Initialize inkycal_rss module""" |     """Initialize inkycal_rss module""" | ||||||
|  |  | ||||||
|     super().__init__(section_size, section_config) |     super().__init__(section_size, section_config) | ||||||
|  |  | ||||||
|     # Module specific parameters |     # Check if required parameters are available in config | ||||||
|     required = ['rss_urls'] |     for param in self.requires: | ||||||
|     for param in required: |  | ||||||
|       if not param in section_config: |       if not param in section_config: | ||||||
|         raise Exception('config is missing {}'.format(param)) |         raise Exception('config is missing {}'.format(param)) | ||||||
|  |  | ||||||
|     # module name |     # parse required config | ||||||
|     self.name = self.__class__.__name__ |     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 |     # give an OK message | ||||||
|     print('{0} loaded'.format(self.name)) |     print('{0} loaded'.format(filename)) | ||||||
|  |  | ||||||
|   def _validate(self): |   def _validate(self): | ||||||
|     """Validate module-specific parameters""" |     """Validate module-specific parameters""" | ||||||
|  |  | ||||||
|     if not isinstance(self.shuffle_feeds, bool): |     if not isinstance(self.shuffle_feeds, bool): | ||||||
|       print('shuffle_feeds has to be a boolean: True/False') |       print('shuffle_feeds has to be a boolean: True/False') | ||||||
|  |  | ||||||
| @@ -55,8 +75,8 @@ class RSS(inkycal_module): | |||||||
|     """Generate image for this module""" |     """Generate image for this module""" | ||||||
|  |  | ||||||
|     # Define new image size with respect to padding |     # Define new image size with respect to padding | ||||||
|     im_width = int(self.width - (self.width * 2 * self.margin_x)) |     im_width = int(self.width - ( 2 * self.padding_x)) | ||||||
|     im_height = int(self.height - (self.height * 2 * self.margin_y)) |     im_height = int(self.height - (2 * self.padding_y)) | ||||||
|     im_size = im_width, im_height |     im_size = im_width, im_height | ||||||
|     logger.info('image size: {} x {} px'.format(im_width, im_height)) |     logger.info('image size: {} x {} px'.format(im_width, im_height)) | ||||||
|  |  | ||||||
| @@ -70,7 +90,6 @@ class RSS(inkycal_module): | |||||||
|     else: |     else: | ||||||
|       raise Exception('Network could not be reached :/') |       raise Exception('Network could not be reached :/') | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Set some parameters for formatting rss feeds |     # Set some parameters for formatting rss feeds | ||||||
|     line_spacing = 1 |     line_spacing = 1 | ||||||
|     line_height = self.font.getsize('hg')[1] + line_spacing |     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 |     # Create list containing all rss-feeds from all rss-feed urls | ||||||
|     parsed_feeds = [] |     parsed_feeds = [] | ||||||
|     for feeds in self.config['rss_urls']: |     for feeds in self.rss_urls: | ||||||
|       text = feedparser.parse(feeds) |       text = feedparser.parse(feeds) | ||||||
|       for posts in text.entries: |       for posts in text.entries: | ||||||
|         parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) |         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 |     del filtered_feeds, parsed_feeds, wrapped, counter, text | ||||||
|  |  | ||||||
|     # Save image of black and colour channel in image-folder |     # Save image of black and colour channel in image-folder | ||||||
|     im_black.save(images+self.name+'.png', 'PNG') |     return im_black, im_colour | ||||||
|     im_colour.save(images+self.name+'_colour.png', 'PNG') |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|   print('running {0} in standalone/debug mode'.format(filename)) |   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) | logger.setLevel(level=logging.ERROR) | ||||||
|  |  | ||||||
|  |  | ||||||
| api = todoist.TodoistAPI('your api key') | class Todoist(inkycal_module): | ||||||
| api.sync() |   """Todoist api class | ||||||
|  |   parses todo's from api-key | ||||||
|  |   """ | ||||||
|  |  | ||||||
| # Print name of author |   name = "Inkycal Todoist" | ||||||
| print(api.state['user']['full_name']+'\n') |  | ||||||
|  |   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: |     self._api = todoist.TodoistAPI(self.config['api_key']) | ||||||
|   print('task: {} is {}'.format(_['content'], 'done' if _['checked'] == 1 else 'not done')) |     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 |   """Weather class | ||||||
|   parses weather details from openweathermap |   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): |   def __init__(self, section_size, section_config): | ||||||
|     """Initialize inkycal_weather module""" |     """Initialize inkycal_weather module""" | ||||||
| @@ -33,35 +87,36 @@ class Weather(inkycal_module): | |||||||
|     super().__init__(section_size, section_config) |     super().__init__(section_size, section_config) | ||||||
|  |  | ||||||
|     # Module specific parameters |     # Module specific parameters | ||||||
|     required = ['api_key','location'] |     for param in self.requires: | ||||||
|     for param in required: |  | ||||||
|       if not param in section_config: |       if not param in section_config: | ||||||
|         raise Exception('config is missing {}'.format(param)) |         raise Exception('config is missing {}'.format(param)) | ||||||
|  |  | ||||||
|     # module name |     # required parameters | ||||||
|     self.name = self.__class__.__name__ |     self.location = self.config['location'] | ||||||
|  |     self.api_key = self.config['api_key'] | ||||||
|  |  | ||||||
|     # module specific parameters |     # optional parameters | ||||||
|     self.owm = pyowm.OWM(self.config['api_key']) |     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.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.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.locale = sys_locale()[0] | ||||||
|     self.weatherfont = ImageFont.truetype(fonts['weathericons-regular-webfont'], |     self.weatherfont = ImageFont.truetype(fonts['weathericons-regular-webfont'], | ||||||
|                                           size = self.fontsize) |                                           size = self.fontsize) | ||||||
|  |  | ||||||
|  |     #self.owm = pyowm.OWM(self.config['api_key']) | ||||||
|     # give an OK message |     # give an OK message | ||||||
|     print('{0} loaded'.format(self.name)) |     print('{0} loaded'.format(filename)) | ||||||
|  |  | ||||||
|   def generate_image(self): |   def generate_image(self): | ||||||
|     """Generate image for this module""" |     """Generate image for this module""" | ||||||
|  |  | ||||||
|     # Define new image size with respect to padding |     # Define new image size with respect to padding | ||||||
|     im_width = int(self.width - (self.width * 2 * self.margin_x)) |     im_width = int(self.width - (2 * self.padding_x)) | ||||||
|     im_height = int(self.height - (self.height * 2 * self.margin_y)) |     im_height = int(self.height - (2 * self.padding_y)) | ||||||
|     im_size = im_width, im_height |     im_size = im_width, im_height | ||||||
|     logger.info('image size: {} x {} px'.format(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, (col6, row1), (col_width, im_height)) | ||||||
|     draw_border(im_black, (col7, 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 |     # return the images ready for the display | ||||||
|     im_black.save(images+self.name+'.png', "PNG") |     return im_black, im_colour | ||||||
|     im_colour.save(images+self.name+'_colour.png', "PNG") |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|   print('running {0} in standalone mode'.format(filename)) |   print('running {0} in standalone mode'.format(filename)) | ||||||
|   | |||||||
| @@ -10,14 +10,16 @@ class inkycal_module(metaclass=abc.ABCMeta): | |||||||
|       callable(subclass.generate_image) or |       callable(subclass.generate_image) or | ||||||
|       NotImplemented) |       NotImplemented) | ||||||
|  |  | ||||||
|   def __init__(self, section_size, section_config): |   def __init__(self, section_config): | ||||||
|     # Initializes base module |     # Initializes base module | ||||||
|     # sets properties shared amongst all sections |     # sets properties shared amongst all sections | ||||||
|     self.config = section_config |     self.config = section_config | ||||||
|     self.width, self.height = section_size |     self.width, self.height = section_config['size'] | ||||||
|     self.fontsize = 12 |  | ||||||
|     self.margin_x = 0.02 |     self.padding_left = self.padding_right = self.config["padding_x"] | ||||||
|     self.margin_y = 0.05 |     self.padding_top = self.padding_bottom = self.config["padding_y"] | ||||||
|  |  | ||||||
|  |     self.fontsize = self.config["fontsize"] | ||||||
|     self.font = ImageFont.truetype( |     self.font = ImageFont.truetype( | ||||||
|       fonts['NotoSans-SemiCondensed'], size = self.fontsize) |       fonts['NotoSans-SemiCondensed'], size = self.fontsize) | ||||||
|  |  | ||||||
| @@ -56,3 +58,33 @@ class inkycal_module(metaclass=abc.ABCMeta): | |||||||
|     # Generate image for this module with specified parameters |     # Generate image for this module with specified parameters | ||||||
|     raise NotImplementedError( |     raise NotImplementedError( | ||||||
|       'The developers were too lazy to implement this function') |       '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... |   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) |   # Initialise the class (do not remove) | ||||||
|   def __init__(self, section_size, section_config): |   def __init__(self, section_size, section_config): | ||||||
|     """Initialize inkycal_rss module""" |     """Initialize your module module""" | ||||||
|  |  | ||||||
|     # Initialise this module via the inkycal_module template (required) |     # Initialise this module via the inkycal_module template (required) | ||||||
|     super().__init__(section_size, section_config) |     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 | feedparser==5.2.1               # parse RSS-feeds | ||||||
| numpy>=1.18.2                   # image pre-processing #pre-installed on Raspbian, omitting | numpy>=1.18.2                   # image pre-processing #pre-installed on Raspbian, omitting | ||||||
| arrow>=0.15.6 	                # time operations | 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