Revert "Inititial commit for release v2.0.0"
This reverts commit 5fa6102c0d
.
This commit is contained in:
parent
5fa6102c0d
commit
ce2c1ba074
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
|
||||
#from inkycal.config.layout import Layout
|
||||
#from inkycal.config.settings_parser import Settings
|
||||
from inkycal.display import Display
|
||||
from inkycal.config.layout import Layout
|
||||
from inkycal.config.settings_parser import Settings
|
||||
|
||||
# All supported inkycal_modules
|
||||
import inkycal.modules.inkycal_agenda
|
||||
import inkycal.modules.inkycal_calendar
|
||||
import inkycal.modules.inkycal_weather
|
||||
import inkycal.modules.inkycal_rss
|
||||
#import inkycal.modules.inkycal_image
|
||||
# import inkycal.modules.inkycal_image
|
||||
# import inkycal.modules.inkycal_server
|
||||
|
||||
# Main file
|
||||
from inkycal.main import Inkycal
|
||||
|
||||
# Added by module adder
|
||||
|
@ -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):
|
||||
display_dimensions = {
|
||||
'9_in_7': (1200, 825),
|
||||
'epd_7_in_5_v3_colour': (880, 528),
|
||||
'epd_7_in_5_v3': (880, 528),
|
||||
'epd_7_in_5_v2_colour': (800, 480),
|
||||
'epd_7_in_5_v2': (800, 480),
|
||||
'epd_7_in_5_colour': (640, 384),
|
||||
@ -114,14 +116,6 @@ class Layout:
|
||||
size = (self.bottom_section_width, self.bottom_section_height)
|
||||
return size
|
||||
|
||||
## def set_info_section(self, value):
|
||||
## """Should a small info section be showed """
|
||||
## if not isinstance(value, bool):
|
||||
## raise ValueError('value has to bee a boolean: True/False')
|
||||
## self.info_section = value
|
||||
## logger.info(('show info section: {}').format(value))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(
|
||||
os.path.basename(__file__).split('.py')[0]))
|
||||
|
@ -23,6 +23,7 @@ class Settings:
|
||||
_supported_update_interval = [10, 15, 20, 30, 60]
|
||||
_supported_display_orientation = ['normal', 'upside_down']
|
||||
_supported_models = [
|
||||
'epd_7_in_5_v3_colour', 'epd_7_in_5_v3',
|
||||
'epd_7_in_5_v2_colour', 'epd_7_in_5_v2',
|
||||
'epd_7_in_5_colour', 'epd_7_in_5',
|
||||
'epd_5_in_83_colour','epd_5_in_83',
|
||||
|
@ -67,7 +67,7 @@ def auto_fontsize(font, max_height):
|
||||
def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
"""Write text on specified image
|
||||
image = on which image should the text be added?
|
||||
xy = (x,y) coordinates as tuple -> (x,y)
|
||||
xy = xy-coordinates as tuple -> (x,y)
|
||||
box_size = size of text-box -> (width,height)
|
||||
text = string (what to write)
|
||||
font = which font to use
|
||||
|
@ -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
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Main class for inkycal Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
from inkycal.display import Display
|
||||
from inkycal import Settings, Layout
|
||||
from inkycal.custom import *
|
||||
|
||||
#from os.path import exists
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
import arrow
|
||||
import time
|
||||
import json
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
@ -27,36 +20,36 @@ except ImportError:
|
||||
print('numpy is not installed! Please install with:')
|
||||
print('pip3 install numpy')
|
||||
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger = logging.getLogger('inkycal')
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
|
||||
class Inkycal:
|
||||
"""Inkycal main class"""
|
||||
|
||||
def __init__(self, settings_path, render=True):
|
||||
"""Initialise Inkycal
|
||||
"""initialise class
|
||||
settings_path = str -> location/folder of settings file
|
||||
render = bool -> show something on the ePaper?
|
||||
"""
|
||||
self._release = '2.0.0'
|
||||
self._release = '2.0.0beta'
|
||||
|
||||
# Check if render was set correctly
|
||||
if render not in [True, False]:
|
||||
# Check if render is boolean
|
||||
if not isinstance(render, bool):
|
||||
raise Exception('render must be True or False, not "{}"'.format(render))
|
||||
self.render = render
|
||||
|
||||
# load settings file - throw an error if file could not be found
|
||||
try:
|
||||
with open(settings_path) as file:
|
||||
settings = json.load(file)
|
||||
self.settings = settings
|
||||
#print(self.settings)
|
||||
# Init settings class
|
||||
self.Settings = Settings(settings_path)
|
||||
|
||||
except FileNotFoundError:
|
||||
print('No settings file found in specified location')
|
||||
print('Please double check your 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
|
||||
@ -64,26 +57,27 @@ class Inkycal:
|
||||
# Load drivers if image should be rendered
|
||||
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
|
||||
self.Display = Display(settings["model"])
|
||||
self.Display = Display(model)
|
||||
|
||||
# check if colours can be rendered
|
||||
self.supports_colour = True if 'colour' in settings['model'] else False
|
||||
# get calibration hours
|
||||
self._calibration_hours = self.Settings.calibration_hours
|
||||
|
||||
# init calibration state
|
||||
# set a check for calibration
|
||||
self._calibration_state = False
|
||||
|
||||
|
||||
|
||||
# WIP
|
||||
for module in settings['modules']:
|
||||
# load+validate settings file. Import and setup specified modules
|
||||
self.active_modules = self.Settings.active_modules()
|
||||
for module in self.active_modules:
|
||||
try:
|
||||
loader = f'from inkycal.modules import {module["name"]}'
|
||||
print(loader)
|
||||
conf = module["config"]
|
||||
#size, conf = module_data['size'], module_data['config']
|
||||
setup = f'self.{module} = {module}(size, conf)'
|
||||
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)))
|
||||
@ -94,9 +88,6 @@ class Inkycal:
|
||||
'Could not find module: "{}". Please try to import manually.'.format(
|
||||
module))
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
# Give an OK message
|
||||
print('loaded inkycal')
|
||||
|
||||
@ -108,7 +99,7 @@ class Inkycal:
|
||||
|
||||
# Check if empty, if empty, use value from settings file
|
||||
if interval_mins == None:
|
||||
interval_mins = self.settings.update_interval
|
||||
interval_mins = self.Settings.update_interval
|
||||
|
||||
# Check if integer
|
||||
if not isinstance(interval_mins, int):
|
||||
@ -136,18 +127,348 @@ class Inkycal:
|
||||
# Return seconds until next update
|
||||
return remaining_time
|
||||
|
||||
def test(self):
|
||||
"""Inkycal test run.
|
||||
Generates images for each module, one by one and prints OK if no
|
||||
problems were found."""
|
||||
print('You are running inkycal v{}'.format(self._release))
|
||||
|
||||
|
||||
print('Running inkycal test-run for {} ePaper'.format(
|
||||
self.Settings.model))
|
||||
|
||||
if self.upside_down == True:
|
||||
print('upside-down mode active')
|
||||
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
print('generating image for {} module...'.format(module), end = '')
|
||||
try:
|
||||
exec(generate_im)
|
||||
print('OK!')
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
print(traceback.format_exc())
|
||||
|
||||
def run(self):
|
||||
"""Runs the main inykcal program nonstop (cannot be stopped anymore!)
|
||||
Will show something on the display if render was set to True"""
|
||||
|
||||
# TODO: printing traceback on display (or at least a smaller message?)
|
||||
# Calibration
|
||||
|
||||
# Get the time of initial run
|
||||
runtime = arrow.now()
|
||||
|
||||
# Function to flip images upside down
|
||||
upside_down = lambda image: image.rotate(180, expand=True)
|
||||
|
||||
# Count the number of times without any errors
|
||||
counter = 1
|
||||
|
||||
# Calculate the max. fontsize for info-section
|
||||
if self.Settings.info_section == True:
|
||||
info_section_height = round(self.Settings.Layout.display_height* (1/95) )
|
||||
self.font = auto_fontsize(ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed']), info_section_height)
|
||||
|
||||
while True:
|
||||
print('Generating images for all modules...')
|
||||
for module in self.active_modules:
|
||||
generate_im = 'self.{0}.generate_image()'.format(module)
|
||||
try:
|
||||
exec(generate_im)
|
||||
except Exception as Error:
|
||||
print('Error!')
|
||||
message = traceback.format_exc()
|
||||
print(message)
|
||||
counter = 0
|
||||
print('OK')
|
||||
|
||||
# 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__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
# 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))
|
||||
|
||||
|
@ -211,11 +211,4 @@ class iCalendar:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
||||
a = iCalendar()
|
||||
now = arrow.now()
|
||||
a.load_url('https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics')
|
||||
a.load_url('https://calendar.yahoo.com/saadnaseer63/37435f792ecb221cdd169d06a518b30f/ycal.ics?id=1670')
|
||||
a.get_events(now, now.shift(weeks=2), a.get_system_tz())
|
||||
a.show_events()
|
||||
print('running {0} in standalone mode'.format(filename))
|
@ -16,60 +16,35 @@ filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
|
||||
|
||||
class Agenda(inkycal_module):
|
||||
"""Agenda class
|
||||
Create agenda and show events from given icalendars
|
||||
"""
|
||||
|
||||
name = "Inkycal Agenda"
|
||||
|
||||
requires = {
|
||||
"ical_urls" : {
|
||||
"label":"iCalendar URL/s, separate multiple ones with a comma",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
optional = {
|
||||
"ical_files" : {
|
||||
"label":"iCalendar filepaths, separated with a comma",
|
||||
"default":[]
|
||||
},
|
||||
|
||||
"date_format":{
|
||||
"label":"Use an arrow-supported token for custom date formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. ddd D MMM",
|
||||
"default": "ddd D MMM",
|
||||
},
|
||||
|
||||
"time_format":{
|
||||
"label":"Use an arrow-supported token for custom time formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_agenda module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
for param in self.equires:
|
||||
# Module specific parameters
|
||||
required = ['week_starts_on', 'ical_urls']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module specific parameters
|
||||
self.date_format = self.config['date_format']
|
||||
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']
|
||||
# class name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# 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.ical_urls = self.config['ical_urls']
|
||||
self.ical_files = []
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(filename))
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
@ -216,6 +191,7 @@ class Agenda(inkycal_module):
|
||||
|
||||
# If no events were found, write only dates and lines
|
||||
else:
|
||||
line_pos = [(0, int(line * line_height)) for line in range(max_lines)]
|
||||
cursor = 0
|
||||
for _ in agenda_events:
|
||||
title = _['title']
|
||||
@ -230,8 +206,9 @@ class Agenda(inkycal_module):
|
||||
|
||||
logger.info('no events found')
|
||||
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png')
|
||||
im_colour.save(images+self.name+'_colour.png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
@ -19,74 +19,42 @@ class Calendar(inkycal_module):
|
||||
Create monthly calendar and show events from given icalendars
|
||||
"""
|
||||
|
||||
name = "Inkycal Calendar"
|
||||
|
||||
optional = {
|
||||
|
||||
"week_starts_on" : {
|
||||
"label":"When does your week start? (default=Monday)",
|
||||
"options": ["Monday", "Sunday"],
|
||||
"default": "Monday"
|
||||
},
|
||||
|
||||
"show_events" : {
|
||||
"label":"Show parsed events? (default = True)",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
"ical_urls" : {
|
||||
"label":"iCalendar URL/s, separate multiple ones with a comma",
|
||||
"default":[]
|
||||
},
|
||||
|
||||
"ical_files" : {
|
||||
"label":"iCalendar filepaths, separated with a comma",
|
||||
"default":[]
|
||||
},
|
||||
|
||||
"date_format":{
|
||||
"label":"Use an arrow-supported token for custom date formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM",
|
||||
"default": "D MMM",
|
||||
},
|
||||
|
||||
"time_format":{
|
||||
"label":"Use an arrow-supported token for custom time formatting "+
|
||||
"see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm",
|
||||
"default": "HH:mm"
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_calendar module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
required = ['week_starts_on']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# module specific parameters
|
||||
self.num_font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size = self.fontsize)
|
||||
self.weekstart = self.config['week_starts_on']
|
||||
self.show_events = self.config['show_events']
|
||||
self.date_format = self.config["date_format"]
|
||||
self.time_format = self.config['time_format']
|
||||
self.show_events = True
|
||||
self.date_format = 'D MMM'
|
||||
self.time_format = "HH:mm"
|
||||
self.language = self.config['language']
|
||||
|
||||
self.timezone = get_system_tz()
|
||||
self.ical_urls = self.config['ical_urls']
|
||||
self.ical_files = self.config['ical_files']
|
||||
self.ical_files = []
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(filename))
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
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_width = int(self.width - (self.width * 2 * self.margin_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.margin_y))
|
||||
im_size = im_width, im_height
|
||||
|
||||
logger.info('Image size: {0}'.format(im_size))
|
||||
@ -112,7 +80,15 @@ class Calendar(inkycal_module):
|
||||
im_width, calendar_height))
|
||||
|
||||
# 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_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
|
||||
range(calendar_cols)]
|
||||
|
||||
now = arrow.now(tz = self.timezone)
|
||||
|
||||
# Set weekstart of calendar to specified weekstart
|
||||
if self.weekstart == "Monday":
|
||||
cal.setfirstweekday(cal.MONDAY)
|
||||
@ -309,8 +283,9 @@ class Calendar(inkycal_module):
|
||||
(im_width, self.font.getsize(symbol)[1]), symbol,
|
||||
font = self.font)
|
||||
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png')
|
||||
im_colour.save(images+self.name+'_colour.png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
@ -1,32 +1,305 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Image module for inkycal Project
|
||||
Image module for Inkycal Project
|
||||
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
|
||||
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?
|
||||
"""----------------------------------------------------------------"""
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.DEBUG)
|
||||
|
||||
# First determine dimensions
|
||||
if mode == 'horizontal':
|
||||
display_width, display_height == display_height, display_width
|
||||
class Inkyimage(inkycal_module):
|
||||
"""Image class
|
||||
display an image from a given path or URL
|
||||
"""
|
||||
_allowed_layout = ['fill', 'center', 'fit', 'auto']
|
||||
_allowed_rotation = [0, 90, 180, 270, 360, 'auto']
|
||||
_allowed_colours = ['bw', 'bwr', 'bwy']
|
||||
|
||||
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 -*-
|
||||
|
||||
"""
|
||||
RSS module for inkyCal Project
|
||||
RSS module for Inky-Calendar Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
@ -25,48 +25,28 @@ class RSS(inkycal_module):
|
||||
parses rss/atom feeds from given urls
|
||||
"""
|
||||
|
||||
name = "Inkycal RSS / Atom"
|
||||
|
||||
requires = {
|
||||
"rss_urls" : {
|
||||
"label":"Please enter ATOM or RSS feed URL/s, separated by a comma",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
optional = {
|
||||
|
||||
"shuffle_feeds": {
|
||||
"label": "Should the parsed RSS feeds be shuffled? (default=True)",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Check if required parameters are available in config
|
||||
for param in self.requires:
|
||||
# Module specific parameters
|
||||
required = ['rss_urls']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# parse required config
|
||||
self.rss_urls = self.config["rss_urls"].split(",")
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# parse optional config
|
||||
self.shuffle_feeds = self.config["shuffle_feeds"]
|
||||
|
||||
# module specific parameters
|
||||
self.shuffle_feeds = True
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(filename))
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
|
||||
if not isinstance(self.shuffle_feeds, bool):
|
||||
print('shuffle_feeds has to be a boolean: True/False')
|
||||
|
||||
@ -75,8 +55,8 @@ class RSS(inkycal_module):
|
||||
"""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_width = int(self.width - (self.width * 2 * self.margin_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.margin_y))
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
@ -90,6 +70,7 @@ class RSS(inkycal_module):
|
||||
else:
|
||||
raise Exception('Network could not be reached :/')
|
||||
|
||||
|
||||
# Set some parameters for formatting rss feeds
|
||||
line_spacing = 1
|
||||
line_height = self.font.getsize('hg')[1] + line_spacing
|
||||
@ -105,7 +86,7 @@ class RSS(inkycal_module):
|
||||
|
||||
# Create list containing all rss-feeds from all rss-feed urls
|
||||
parsed_feeds = []
|
||||
for feeds in self.rss_urls:
|
||||
for feeds in self.config['rss_urls']:
|
||||
text = feedparser.parse(feeds)
|
||||
for posts in text.entries:
|
||||
parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary))
|
||||
@ -146,8 +127,8 @@ class RSS(inkycal_module):
|
||||
del filtered_feeds, parsed_feeds, wrapped, counter, text
|
||||
|
||||
# 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__':
|
||||
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)
|
||||
|
||||
|
||||
class Todoist(inkycal_module):
|
||||
"""Todoist api class
|
||||
parses todo's from api-key
|
||||
"""
|
||||
api = todoist.TodoistAPI('your api key')
|
||||
api.sync()
|
||||
|
||||
name = "Inkycal Todoist"
|
||||
|
||||
requires = {
|
||||
'api_key': {
|
||||
"label":"Please enter your Todoist API-key",
|
||||
},
|
||||
}
|
||||
|
||||
optional = {
|
||||
'project_filter': {
|
||||
"label":"Show Todos only from following project (separated by a comma). Leave empty to show "+
|
||||
"todos from all projects",
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
for param in self.requires:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
# Print name of author
|
||||
print(api.state['user']['full_name']+'\n')
|
||||
|
||||
|
||||
# module specific parameters
|
||||
self.api_key = self.config['api_key']
|
||||
self.project_filter = self.config['project_filter']# only show todos from these projects
|
||||
tasks = (task.data for task in api.state['items'])
|
||||
|
||||
self._api = todoist.TodoistAPI(self.config['api_key'])
|
||||
self._api.sync()
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
if not isinstance(self.api_key, str):
|
||||
print('api_key has to be a string: "Yourtopsecretkey123" ')
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = int(self.width - (2 * self.padding_x))
|
||||
im_height = int(self.height - (2 * self.padding_y))
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size = im_size, color = 'white')
|
||||
im_colour = Image.new('RGB', size = im_size, color = 'white')
|
||||
|
||||
# Check if internet is available
|
||||
if internet_available() == True:
|
||||
logger.info('Connection test passed')
|
||||
else:
|
||||
raise Exception('Network could not be reached :/')
|
||||
|
||||
# Set some parameters for formatting todos
|
||||
line_spacing = 1
|
||||
line_height = self.font.getsize('hg')[1] + line_spacing
|
||||
line_width = im_width
|
||||
max_lines = (im_height // (self.font.getsize('hg')[1] + line_spacing))
|
||||
|
||||
# Calculate padding from top so the lines look centralised
|
||||
spacing_top = int( im_height % line_height / 2 )
|
||||
|
||||
# Calculate line_positions
|
||||
line_positions = [
|
||||
(0, spacing_top + _ * line_height ) for _ in range(max_lines)]
|
||||
|
||||
#------------------------------------------------------------------------##
|
||||
# Get all projects by name and id
|
||||
all_projects = {project['name']: project['id']
|
||||
for project in self._api.projects.all()}
|
||||
|
||||
# Check if project from filter could be found
|
||||
if self.project_filter:
|
||||
for project in self.project_filter:
|
||||
if project not in all_projects:
|
||||
print('Could not find a project named {}'.format(project))
|
||||
self.project_filter.remove(project)
|
||||
|
||||
# function for extracting project names from tasks
|
||||
get_project_name = lambda task: (self._api.projects.get_data(
|
||||
task['project_id'])['project']['name'])
|
||||
|
||||
# If the filter is empty, parse all tasks which are not yet done
|
||||
if self.project_filter:
|
||||
tasks = (task.data for task in self._api.state['items']
|
||||
if (task['checked'] == 0) and
|
||||
(get_project_name(task) in self.project_filter))
|
||||
|
||||
# If filter is not empty, parse undone tasks in only those projects
|
||||
else:
|
||||
tasks = (task.data for task in self._api.state['items'] if
|
||||
(task['checked'] == 0))
|
||||
|
||||
# Simplify the tasks for faster processing
|
||||
simplified = [{'name':task['content'],
|
||||
'due':task['due'],
|
||||
'priority':task['priority'],
|
||||
'project_id':task['project_id']}
|
||||
for task in tasks]
|
||||
|
||||
# Group tasks by project name
|
||||
grouped = {}
|
||||
|
||||
if self.project_filter:
|
||||
for project in self.project_filter:
|
||||
project_id = all_projects[project]
|
||||
grouped[ project ] = [
|
||||
task for task in simplified if task['project_id'] == project_id]
|
||||
else:
|
||||
for project in all_projects:
|
||||
project_id = all_projects[project]
|
||||
grouped[ project ] = [
|
||||
task for task in simplified if task['project_id'] == project_id]
|
||||
|
||||
# Print tasks sorted by groups
|
||||
for project, tasks in grouped.items():
|
||||
print('*', project)
|
||||
for task in tasks:
|
||||
print('• {} {}'.format(
|
||||
task['due']['string'] if task['due'] != None else '', task['name']))
|
||||
|
||||
|
||||
## # Write rss-feeds on image
|
||||
## for _ in range(len(filtered_feeds)):
|
||||
## write(im_black, line_positions[_], (line_width, line_height),
|
||||
## filtered_feeds[_], font = self.font, alignment= 'left')
|
||||
|
||||
|
||||
|
||||
# Cleanup ---------------------------
|
||||
# del grouped, parsed_feeds, wrapped, counter, text
|
||||
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
config = {'api_key':'4e166367dcafdd60e6a9f4cbed598d578bf2c359'}
|
||||
size = (480, 100)
|
||||
a = Todoist(size, config)
|
||||
b,c = a.generate_image()
|
||||
for _ in tasks:
|
||||
print('task: {} is {}'.format(_['content'], 'done' if _['checked'] == 1 else 'not done'))
|
||||
|
@ -26,60 +26,6 @@ class Weather(inkycal_module):
|
||||
"""Weather class
|
||||
parses weather details from openweathermap
|
||||
"""
|
||||
#TODO: automatic setup of pyowm by location id if location is numeric
|
||||
|
||||
name = "Inkycal Weather (openweathermap)"
|
||||
|
||||
requires = {
|
||||
|
||||
"api_key" : {
|
||||
"label":"Please enter openweathermap api-key. You can create one for free on openweathermap",
|
||||
},
|
||||
|
||||
"location": {
|
||||
"label":"Please enter your location in the following format: City, Country-Code"
|
||||
}
|
||||
}
|
||||
|
||||
optional = {
|
||||
|
||||
"round_temperature": {
|
||||
"label":"Round temperature to the nearest degree?",
|
||||
"options": [True, False],
|
||||
"default" : True
|
||||
},
|
||||
|
||||
"round_windspeed": {
|
||||
"label":"Round windspeed?",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
"forecast_interval": {
|
||||
"label":"Please select the forecast interval",
|
||||
"options": ["daily", "hourly"],
|
||||
"default": "daily"
|
||||
},
|
||||
|
||||
"units": {
|
||||
"label": "Which units should be used?",
|
||||
"options": ["metric", "imperial"],
|
||||
"default": "metric"
|
||||
},
|
||||
|
||||
"hour_format": {
|
||||
"label": "Which hour format do you prefer?",
|
||||
"options": [12, 24],
|
||||
"default": 24
|
||||
},
|
||||
|
||||
"use_beaufort": {
|
||||
"label": "Use beaufort scale for windspeed?",
|
||||
"options": [True, False],
|
||||
"default": True
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_weather module"""
|
||||
@ -87,36 +33,35 @@ class Weather(inkycal_module):
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
# Module specific parameters
|
||||
for param in self.requires:
|
||||
required = ['api_key','location']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# required parameters
|
||||
self.location = self.config['location']
|
||||
self.api_key = self.config['api_key']
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# optional parameters
|
||||
self.round_temperature = self.config['round_temperature']
|
||||
self.round_windspeed = self.config['round_windspeed']
|
||||
self.forecast_interval = self.config['forecast_interval']
|
||||
# module specific parameters
|
||||
self.owm = pyowm.OWM(self.config['api_key'])
|
||||
self.units = self.config['units']
|
||||
self.hour_format = self.config['hour_format']
|
||||
self.use_beaufort = self.config['use_beaufort']
|
||||
self.hour_format = self.config['hours']
|
||||
self.timezone = get_system_tz()
|
||||
self.round_temperature = True
|
||||
self.round_windspeed = True
|
||||
self.use_beaufort = True
|
||||
self.forecast_interval = 'daily' # daily # hourly
|
||||
self.locale = sys_locale()[0]
|
||||
self.weatherfont = ImageFont.truetype(fonts['weathericons-regular-webfont'],
|
||||
size = self.fontsize)
|
||||
|
||||
#self.owm = pyowm.OWM(self.config['api_key'])
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(filename))
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
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_width = int(self.width - (self.width * 2 * self.margin_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.margin_y))
|
||||
im_size = 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, (col7, row1), (col_width, im_height))
|
||||
|
||||
# return the images ready for the display
|
||||
return im_black, im_colour
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png', "PNG")
|
||||
im_colour.save(images+self.name+'_colour.png', "PNG")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(filename))
|
||||
|
@ -10,16 +10,14 @@ class inkycal_module(metaclass=abc.ABCMeta):
|
||||
callable(subclass.generate_image) or
|
||||
NotImplemented)
|
||||
|
||||
def __init__(self, section_config):
|
||||
def __init__(self, section_size, section_config):
|
||||
# Initializes base module
|
||||
# sets properties shared amongst all sections
|
||||
self.config = section_config
|
||||
self.width, self.height = section_config['size']
|
||||
|
||||
self.padding_left = self.padding_right = self.config["padding_x"]
|
||||
self.padding_top = self.padding_bottom = self.config["padding_y"]
|
||||
|
||||
self.fontsize = self.config["fontsize"]
|
||||
self.width, self.height = section_size
|
||||
self.fontsize = 12
|
||||
self.margin_x = 0.02
|
||||
self.margin_y = 0.05
|
||||
self.font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size = self.fontsize)
|
||||
|
||||
@ -58,33 +56,3 @@ class inkycal_module(metaclass=abc.ABCMeta):
|
||||
# Generate image for this module with specified parameters
|
||||
raise NotImplementedError(
|
||||
'The developers were too lazy to implement this function')
|
||||
|
||||
@classmethod
|
||||
def get_config(cls):
|
||||
# Get the config of this module for the web-ui
|
||||
# Do not change
|
||||
try:
|
||||
|
||||
if hasattr(cls, 'requires'):
|
||||
for each in cls.requires:
|
||||
if not "label" in cls.requires[each]:
|
||||
raise Exception("no label found for {}".format(each))
|
||||
|
||||
if hasattr(cls, 'optional'):
|
||||
for each in cls.optional:
|
||||
if not "label" in cls.optional[each]:
|
||||
raise Exception("no label found for {}".format(each))
|
||||
|
||||
conf = {
|
||||
"name": cls.__name__,
|
||||
"name_str": cls.name,
|
||||
"requires": cls.requires if hasattr(cls, 'requires') else {},
|
||||
"optional": cls.optional if hasattr(cls, 'optional') else {},
|
||||
}
|
||||
return conf
|
||||
except:
|
||||
raise Exception(
|
||||
'Ohoh, something went wrong while trying to get the config of this module')
|
||||
|
||||
|
||||
|
||||
|
@ -60,21 +60,9 @@ class Simple(inkycal_module):
|
||||
Explain what this module does...
|
||||
"""
|
||||
|
||||
# name is the name that will be shown on the web-ui
|
||||
# may be same or different to the class name (Do not remove this)
|
||||
name = "My own module"
|
||||
|
||||
# create a dictionary that specifies what your module absolutely needs
|
||||
# to run correctly
|
||||
# Use the following format -> "key" : "info about this key for web-ui"
|
||||
# You can add as many required entries as you like
|
||||
requires = {
|
||||
"module_parameter" : "Short info about this parameter, shown on the web-ui",
|
||||
}
|
||||
|
||||
# Initialise the class (do not remove)
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize your module module"""
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
# Initialise this module via the inkycal_module template (required)
|
||||
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
|
||||
numpy>=1.18.2 # image pre-processing #pre-installed on Raspbian, omitting
|
||||
arrow>=0.15.6 # time operations
|
||||
#jsmin>=2.2.2 # parsing settings.jsonc file
|
||||
flask==1.1.2 # required for web-ui
|
||||
Flask-WTF==0.14.3 # required for web-ui
|
||||
jsmin>=2.2.2 # parsing settings.jsonc file
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user