Revert "Inititial commit for release v2.0.0"
This reverts commit 5fa6102c0d.
			
			
This commit is contained in:
		
							
								
								
									
										173
									
								
								Installer.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								Installer.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | |||||||
|  | #!/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,17 +1,15 @@ | |||||||
| # 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  |  | ||||||
|   | |||||||
| @@ -1,474 +0,0 @@ | |||||||
| 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,6 +24,8 @@ 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), | ||||||
| @@ -114,14 +116,6 @@ 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,6 +23,7 @@ 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 = (x,y) coordinates as tuple -> (x,y) |   xy = xy-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 .display import Display | from .epaper import Display | ||||||
|   | |||||||
| @@ -1,130 +0,0 @@ | |||||||
| #!/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,19 +1,12 @@ | |||||||
| #!/usr/bin/python3 | from inkycal import Settings, Layout | ||||||
| # -*- coding: utf-8 -*- |  | ||||||
|  |  | ||||||
| """ |  | ||||||
| Main class for inkycal Project |  | ||||||
| Copyright by aceisace |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| from inkycal.display import Display |  | ||||||
| from inkycal.custom import * | from inkycal.custom import * | ||||||
|  |  | ||||||
|  | #from os.path import exists | ||||||
| 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 | ||||||
| @@ -27,36 +20,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') | ||||||
|  |  | ||||||
| filename = os.path.basename(__file__).split('.py')[0] | logger = logging.getLogger('inkycal') | ||||||
| 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 Inkycal |     """initialise class | ||||||
|     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.0' |     self._release = '2.0.0beta' | ||||||
|  |  | ||||||
|     # Check if render was set correctly |     # Check if render is boolean | ||||||
|     if render not in [True, False]: |     if not isinstance(render, bool): | ||||||
|       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 | ||||||
|  |  | ||||||
|     # load settings file - throw an error if file could not be found |     # Init settings class | ||||||
|     try: |     self.Settings = Settings(settings_path) | ||||||
|       with open(settings_path) as file: |  | ||||||
|         settings = json.load(file) |  | ||||||
|         self.settings = settings |  | ||||||
|         #print(self.settings) |  | ||||||
|  |  | ||||||
|     except FileNotFoundError: |     # Check if display support colour | ||||||
|       print('No settings file found in specified location') |     self.supports_colour = self.Settings.Layout.supports_colour | ||||||
|       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 | ||||||
| @@ -64,26 +57,27 @@ class Inkycal: | |||||||
|     # Load drivers if image should be rendered |     # Load drivers if image should be rendered | ||||||
|     if self.render == True: |     if self.render == True: | ||||||
|  |  | ||||||
|       # Init Display class with model in settings file |       # Get model and check if colour can be rendered | ||||||
|  |       model= self.Settings.model | ||||||
|  |  | ||||||
|  |       # Init Display class | ||||||
|       from inkycal.display import Display |       from inkycal.display import Display | ||||||
|       self.Display = Display(settings["model"]) |       self.Display = Display(model) | ||||||
|  |  | ||||||
|       # check if colours can be rendered |       # get calibration hours | ||||||
|       self.supports_colour = True if 'colour' in settings['model'] else False |       self._calibration_hours = self.Settings.calibration_hours | ||||||
|  |  | ||||||
|       # init calibration state |       # set a check for calibration | ||||||
|       self._calibration_state = False |       self._calibration_state = False | ||||||
|  |  | ||||||
|  |     # load+validate settings file. Import and setup specified modules | ||||||
|  |     self.active_modules = self.Settings.active_modules() | ||||||
|     # WIP |     for module in self.active_modules: | ||||||
|     for module in settings['modules']: |  | ||||||
|       try: |       try: | ||||||
|         loader = f'from inkycal.modules import {module["name"]}' |         loader = 'from inkycal.modules import {0}'.format(module) | ||||||
|         print(loader) |         module_data = self.Settings.get_config(module) | ||||||
|         conf = module["config"] |         size, conf = module_data['size'], module_data['config'] | ||||||
|         #size, conf = module_data['size'], module_data['config'] |         setup = 'self.{} = {}(size, conf)'.format(module, module) | ||||||
|         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))) | ||||||
| @@ -94,9 +88,6 @@ 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') | ||||||
|  |  | ||||||
| @@ -108,7 +99,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): | ||||||
| @@ -136,18 +127,348 @@ 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: | ||||||
|  |       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 | ||||||
|  |     """ | ||||||
|  |  | ||||||
| if __name__ == '__main__': |     # Path for modules | ||||||
|   print('running {0} in standalone/debug mode'.format(filename)) |     _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)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -211,11 +211,4 @@ 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,60 +16,35 @@ 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 | ||||||
|     for param in self.equires: |     required = ['week_starts_on', 'ical_urls'] | ||||||
|  |     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 specific parameters |     # class name | ||||||
|     self.date_format = self.config['date_format'] |     self.name = self.__class__.__name__ | ||||||
|     self.time_format = self.config['time_format'] |  | ||||||
|     self.language = self.config['language'] |  | ||||||
|     self.ical_urls = self.config['ical_urls'] |  | ||||||
|     self.ical_files = self.config['ical_files'] |  | ||||||
|  |  | ||||||
|  |     # module specific parameters | ||||||
|  |     self.date_format = 'ddd D MMM' | ||||||
|  |     self.time_format = "HH:mm" | ||||||
|  |     self.language = self.config['language'] | ||||||
|     self.timezone = get_system_tz() |     self.timezone = get_system_tz() | ||||||
|  |     self.ical_urls = self.config['ical_urls'] | ||||||
|  |     self.ical_files = [] | ||||||
|  |  | ||||||
|     # give an OK message |     # give an OK message | ||||||
|     print('{0} loaded'.format(filename)) |     print('{0} loaded'.format(self.name)) | ||||||
|  |  | ||||||
|   def _validate(self): |   def _validate(self): | ||||||
|     """Validate module-specific parameters""" |     """Validate module-specific parameters""" | ||||||
| @@ -216,6 +191,7 @@ 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'] | ||||||
| @@ -230,8 +206,9 @@ class Agenda(inkycal_module): | |||||||
|  |  | ||||||
|       logger.info('no events found') |       logger.info('no events found') | ||||||
|  |  | ||||||
|     # return the images ready for the display |     # Save image of black and colour channel in image-folder | ||||||
|     return im_black, im_colour |     im_black.save(images+self.name+'.png') | ||||||
|  |     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,74 +19,42 @@ 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 = self.config['show_events'] |     self.show_events = True | ||||||
|     self.date_format = self.config["date_format"] |     self.date_format = 'D MMM' | ||||||
|     self.time_format = self.config['time_format'] |     self.time_format = "HH:mm" | ||||||
|     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.config['ical_files'] |     self.ical_files = [] | ||||||
|  |  | ||||||
|     # give an OK message |     # give an OK message | ||||||
|     print('{0} loaded'.format(filename)) |     print('{0} loaded'.format(self.name)) | ||||||
|  |  | ||||||
|   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 - (2 * self.padding_x)) |     im_width = int(self.width - (self.width * 2 * self.margin_x)) | ||||||
|     im_height = int(self.height - (2 * self.padding_y)) |     im_height = int(self.height - (self.height * 2 * self.margin_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)) | ||||||
| @@ -112,7 +80,15 @@ class Calendar(inkycal_module): | |||||||
|         im_width, calendar_height)) |         im_width, calendar_height)) | ||||||
|  |  | ||||||
|     # Create grid and calculate icon sizes |     # Create grid and calculate icon sizes | ||||||
|     calendar_rows, calendar_cols = 6, 7 |     now = arrow.now(tz = self.timezone) | ||||||
|  |     monthstart = now.span('month')[0].weekday() | ||||||
|  |     monthdays = now.ceil('month').day | ||||||
|  |  | ||||||
|  |     if monthstart > 4 and monthdays == 31: | ||||||
|  |         calendar_rows, calendar_cols = 7, 7 | ||||||
|  |     else: | ||||||
|  |         calendar_rows, calendar_cols = 6, 7 | ||||||
|  |  | ||||||
|     icon_width = im_width // calendar_cols |     icon_width = im_width // calendar_cols | ||||||
|     icon_height = calendar_height // calendar_rows |     icon_height = calendar_height // calendar_rows | ||||||
|  |  | ||||||
| @@ -130,8 +106,6 @@ 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) | ||||||
| @@ -309,8 +283,9 @@ 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) | ||||||
|  |  | ||||||
|     # return the images ready for the display |     # Save image of black and colour channel in image-folder | ||||||
|     return im_black, im_colour |     im_black.save(images+self.name+'.png') | ||||||
|  |     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,32 +1,305 @@ | |||||||
| #!/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 os import path | from inkycal.modules.template import inkycal_module | ||||||
|  | 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] | ||||||
| #path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' | logger = logging.getLogger(filename) | ||||||
| #path  ='/home/pi/Inky-Calendar/images/canvas.png' | logger.setLevel(level=logging.DEBUG) | ||||||
| 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? |  | ||||||
| """----------------------------------------------------------------""" |  | ||||||
|  |  | ||||||
| # First determine dimensions | class Inkyimage(inkycal_module): | ||||||
| if mode == 'horizontal': |   """Image class | ||||||
|   display_width, display_height == display_height, display_width |   display an image from a given path or URL | ||||||
|  |   """ | ||||||
|  |   _allowed_layout = ['fill', 'center', 'fit', 'auto'] | ||||||
|  |   _allowed_rotation = [0, 90, 180, 270, 360, 'auto'] | ||||||
|  |   _allowed_colours = ['bw', 'bwr', 'bwy'] | ||||||
|  |  | ||||||
|  |   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') |  | ||||||
|   | |||||||
| @@ -1,305 +0,0 @@ | |||||||
| #!/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 inkyCal Project | RSS module for Inky-Calendar Project | ||||||
| Copyright by aceisace | Copyright by aceisace | ||||||
| """ | """ | ||||||
|  |  | ||||||
| @@ -25,48 +25,28 @@ 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) | ||||||
|  |  | ||||||
|     # Check if required parameters are available in config |     # Module specific parameters | ||||||
|     for param in self.requires: |     required = ['rss_urls'] | ||||||
|  |     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)) | ||||||
|  |  | ||||||
|     # parse required config |     # module name | ||||||
|     self.rss_urls = self.config["rss_urls"].split(",") |     self.name = self.__class__.__name__ | ||||||
|  |  | ||||||
|     # parse optional config |     # module specific parameters | ||||||
|     self.shuffle_feeds = self.config["shuffle_feeds"] |     self.shuffle_feeds = True | ||||||
|                     |  | ||||||
|  |  | ||||||
|     # give an OK message |     # give an OK message | ||||||
|     print('{0} loaded'.format(filename)) |     print('{0} loaded'.format(self.name)) | ||||||
|  |  | ||||||
|   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') | ||||||
|  |  | ||||||
| @@ -75,8 +55,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 - ( 2 * self.padding_x)) |     im_width = int(self.width - (self.width * 2 * self.margin_x)) | ||||||
|     im_height = int(self.height - (2 * self.padding_y)) |     im_height = int(self.height - (self.height * 2 * self.margin_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)) | ||||||
|  |  | ||||||
| @@ -90,6 +70,7 @@ 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 | ||||||
| @@ -105,7 +86,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.rss_urls: |     for feeds in self.config['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)) | ||||||
| @@ -146,8 +127,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 | ||||||
|     return im_black, im_colour |     im_black.save(images+self.name+'.png', 'PNG') | ||||||
|  |     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()) |  | ||||||
|   | |||||||
| @@ -1,41 +0,0 @@ | |||||||
| #!/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,157 +20,14 @@ logger = logging.getLogger(filename) | |||||||
| logger.setLevel(level=logging.ERROR) | logger.setLevel(level=logging.ERROR) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Todoist(inkycal_module): | api = todoist.TodoistAPI('your api key') | ||||||
|   """Todoist api class | api.sync() | ||||||
|   parses todo's from api-key |  | ||||||
|   """ |  | ||||||
|  |  | ||||||
|   name = "Inkycal Todoist" | # Print name of author | ||||||
|  | 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)) |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     # module specific parameters | tasks = (task.data for task in api.state['items']) | ||||||
|     self.api_key = self.config['api_key'] |  | ||||||
|     self.project_filter = self.config['project_filter']# only show todos from these projects |  | ||||||
|  |  | ||||||
|     self._api = todoist.TodoistAPI(self.config['api_key']) | for _ in tasks: | ||||||
|     self._api.sync() |   print('task: {} is {}'.format(_['content'], 'done' if _['checked'] == 1 else 'not done')) | ||||||
|  |  | ||||||
|     # 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,60 +26,6 @@ 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""" | ||||||
| @@ -87,36 +33,35 @@ class Weather(inkycal_module): | |||||||
|     super().__init__(section_size, section_config) |     super().__init__(section_size, section_config) | ||||||
|  |  | ||||||
|     # Module specific parameters |     # Module specific parameters | ||||||
|     for param in self.requires: |     required = ['api_key','location'] | ||||||
|  |     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)) | ||||||
|  |  | ||||||
|     # required parameters |     # module name | ||||||
|     self.location = self.config['location'] |     self.name = self.__class__.__name__ | ||||||
|     self.api_key = self.config['api_key'] |  | ||||||
|  |  | ||||||
|     # optional parameters |     # module specific parameters | ||||||
|     self.round_temperature = self.config['round_temperature'] |     self.owm = pyowm.OWM(self.config['api_key']) | ||||||
|     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['hour_format'] |     self.hour_format = self.config['hours'] | ||||||
|     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(filename)) |     print('{0} loaded'.format(self.name)) | ||||||
|  |  | ||||||
|   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 - (2 * self.padding_x)) |     im_width = int(self.width - (self.width * 2 * self.margin_x)) | ||||||
|     im_height = int(self.height - (2 * self.padding_y)) |     im_height = int(self.height - (self.height * 2 * self.margin_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)) | ||||||
|  |  | ||||||
| @@ -477,8 +422,9 @@ 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)) | ||||||
|  |  | ||||||
|     # return the images ready for the display |   # Save image of black and colour channel in image-folder | ||||||
|     return im_black, im_colour |     im_black.save(images+self.name+'.png', "PNG") | ||||||
|  |     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,16 +10,14 @@ class inkycal_module(metaclass=abc.ABCMeta): | |||||||
|       callable(subclass.generate_image) or |       callable(subclass.generate_image) or | ||||||
|       NotImplemented) |       NotImplemented) | ||||||
|  |  | ||||||
|   def __init__(self, section_config): |   def __init__(self, section_size, 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_config['size'] |     self.width, self.height = section_size | ||||||
|  |     self.fontsize = 12 | ||||||
|     self.padding_left = self.padding_right = self.config["padding_x"] |     self.margin_x = 0.02 | ||||||
|     self.padding_top = self.padding_bottom = self.config["padding_y"] |     self.margin_y = 0.05 | ||||||
|  |  | ||||||
|     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) | ||||||
|  |  | ||||||
| @@ -58,33 +56,3 @@ 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,21 +60,9 @@ 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 your module module""" |     """Initialize inkycal_rss 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
									
									
									
									
									
								
							
							
						
						
									
										474
									
								
								inkycal/old.py
									
									
									
									
									
								
							| @@ -1,474 +0,0 @@ | |||||||
| 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)) |  | ||||||
|  |  | ||||||
| @@ -1,62 +0,0 @@ | |||||||
| { |  | ||||||
|     "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,6 +5,4 @@ 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 |  | ||||||
|   | |||||||
| @@ -1,7 +0,0 @@ | |||||||
| from flask import Flask |  | ||||||
| from config import Config |  | ||||||
|  |  | ||||||
| app = Flask(__name__) |  | ||||||
| app.config.from_object(Config) |  | ||||||
|  |  | ||||||
| from app import routes |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| 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') |  | ||||||
| @@ -1,108 +0,0 @@ | |||||||
| 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) |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,59 +0,0 @@ | |||||||
| <!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> |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| {% extends "base.html" %} |  | ||||||
|  |  | ||||||
| {% block content %} |  | ||||||
|  |  | ||||||
|     <body> |  | ||||||
|         <div class="container"><h4>Welcome to inkycal config portal</h4></div> |  | ||||||
|     </body> |  | ||||||
|  |  | ||||||
| {% endblock %} |  | ||||||
| @@ -1,445 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| {% 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 %} |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| import os |  | ||||||
|  |  | ||||||
| class Config(object): |  | ||||||
|     SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' |  | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										837
									
								
								settings-UI.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,837 @@ | |||||||
|  | <!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