Revert "Inititial commit for release v2.0.0"

This reverts commit 5fa6102c0d.
This commit is contained in:
Ace 2020-11-09 17:44:13 +01:00
parent 5fa6102c0d
commit ce2c1ba074
35 changed files with 1768 additions and 2660 deletions

173
Installer.sh Normal file
View 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

View File

@ -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

View File

@ -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))

View File

@ -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]))

View File

@ -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',

View File

@ -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

View File

@ -1 +1 @@
from .display import Display
from .epaper import Display

View File

@ -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")

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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')

View File

@ -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()

View File

@ -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())

View File

@ -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.')

View 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'))

View File

@ -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))

View File

@ -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')

View File

@ -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)

View File

@ -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))

View File

@ -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"
}
]
}

View File

@ -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

View 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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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>

View File

@ -1,9 +0,0 @@
{% extends "base.html" %}
{% block content %}
<body>
<div class="container"><h4>Welcome to inkycal config portal</h4></div>
</body>
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -1,4 +0,0 @@
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

View File

@ -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
View 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>