Folder restructuring
Reorganised folders and files
This commit is contained in:
parent
47a327cddc
commit
fd9fcbcc8d
5
.gitignore.txt
Normal file
5
.gitignore.txt
Normal file
@ -0,0 +1,5 @@
|
||||
*.pyc
|
||||
__pycache__/
|
||||
_build
|
||||
_static
|
||||
_templates
|
@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script for updating the Inky-Calendar software. This will automatically
|
||||
# transfer the user's own details with the placeholders in the settings.py file
|
||||
|
||||
# To-do: Delete the old settings.py file after all operations are done
|
||||
|
||||
in="/home/pi/settings.py.old"
|
||||
out="/home/pi/Inky-Calendar/Calendar/settings.py"
|
||||
|
||||
# replace template iCalendar URLs with user-defined URLs
|
||||
sed -n -e "/^ical_urls/r $in" -i -e "/^ical_urls/d" $out
|
||||
sed -n -e "/^rss_feeds/r $in" -i -e "/^rss_feeds/d" $out
|
||||
sed -n -e "/^update_interval/r $in" -i -e "/^update_interval/d" $out
|
||||
sed -n -e "/^api_key/r $in" -i -e "/^api_key/d" $out
|
||||
sed -n -e "/^location/r $in" -i -e "/^location/d" $out
|
||||
sed -n -e "/^week_starts_on/r $in" -i -e "/^week_starts_on/d" $out
|
||||
sed -n -e "/^calibration_hours/r $in" -i -e "/^calibration_hours/d" $out
|
||||
sed -n -e "/^display_colours/r $in" -i -e "/^display_colours/d" $out
|
||||
sed -n -e "/^language/r $in" -i -e "/^language/d" $out
|
||||
sed -n -e "/^units/r $in" -i -e "/^units/d" $out
|
||||
sed -n -e "/^hours/r $in" -i -e "/^hours/d" $out
|
||||
sed -n -e "/^top_section/r $in" -i -e "/^top_section/d" $out
|
||||
sed -n -e "/^middle_section/r $in" -i -e "/^middle_section/d" $out
|
||||
sed -n -e "/^bottom_section/r $in" -i -e "/^bottom_section/d" $out
|
||||
|
||||
echo -e 'All operations done'
|
45
inkycal/Inkycal.py
Normal file
45
inkycal/Inkycal.py
Normal file
@ -0,0 +1,45 @@
|
||||
from importlib import import_module
|
||||
|
||||
from inkycal.configuration.settings_parser import inkycal_settings as settings
|
||||
from inkycal.display.layout import inkycal_layout as layout
|
||||
|
||||
|
||||
|
||||
##modules = settings.which_modules()
|
||||
##for module in modules:
|
||||
## if module == 'inkycal_rss':
|
||||
## module = import_module('inkycal.modules.'+module)
|
||||
## #import_module('modules.'+module)
|
||||
##print(module)
|
||||
|
||||
settings_file = '/home/pi/Desktop/settings.json'
|
||||
|
||||
|
||||
class inkycal:
|
||||
def __init__(self, settings_file_path):
|
||||
"""Load settings file from path"""
|
||||
# Load settings file
|
||||
self.settings = settings(settings_file_path)
|
||||
self.model = self.settings.model
|
||||
|
||||
def create_canvas(self):
|
||||
"""Create a canvas with same size as the specified model"""
|
||||
self.layout = layout(model=self.model)
|
||||
|
||||
def create_custom_canvas(self, width=None, height=None,
|
||||
supports_colour=False):
|
||||
"""Create a custom canvas by specifying height and width"""
|
||||
self.layout = layout(model=model, width=width, height=height,
|
||||
supports_colour=supports_colour)
|
||||
|
||||
def create_sections(self):
|
||||
"""Create sections with default sizes"""
|
||||
self.layout.create_sections()
|
||||
|
||||
def create_custom_sections(self, top_section=0.10, middle_section=0.65,
|
||||
bottom_section=0.25):
|
||||
"""Create custom-sized sections in the canvas"""
|
||||
self.layout.create_sections(top_section=top_section,
|
||||
middle_section=middle_section,
|
||||
bottom_section=bottom_section)
|
||||
|
0
inkycal/__init__.py
Normal file
0
inkycal/__init__.py
Normal file
2
inkycal/configuration/__init__.py
Normal file
2
inkycal/configuration/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .settings_parser import inkycal_settings
|
||||
print('loaded settings')
|
42
inkycal/configuration/settings.json
Normal file
42
inkycal/configuration/settings.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"language": "en",
|
||||
"units": "metric",
|
||||
"hours": 24,
|
||||
"model": "epd_7_in_5_v2_colour",
|
||||
"update_interval": 60,
|
||||
"calibration_hours": [
|
||||
0,
|
||||
12,
|
||||
18
|
||||
],
|
||||
"display_orientation": "normal",
|
||||
"panels": [
|
||||
{
|
||||
"location": "top",
|
||||
"type": "inkycal_weather",
|
||||
"config": {
|
||||
"api_key": "topsecret",
|
||||
"location": "Stuttgart, DE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": "middle",
|
||||
"type": "inkycal_calendar",
|
||||
"config": {
|
||||
"week_starts_on": "Monday",
|
||||
"ical_urls": [
|
||||
"https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"location": "bottom",
|
||||
"type": "inkycal_rss",
|
||||
"config": {
|
||||
"rss_urls": [
|
||||
"http://feeds.bbci.co.uk/news/world/rss.xml#"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
137
inkycal/configuration/settings_parser.py
Normal file
137
inkycal/configuration/settings_parser.py
Normal file
@ -0,0 +1,137 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Json settings parser. Currently in alpha!
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
import json
|
||||
from os import chdir #Ad-hoc
|
||||
|
||||
# TODO:
|
||||
# Check of jsmin can/should be used to parse jsonc settings file
|
||||
# Remove check of fixed settings file location. Ask user to specify path
|
||||
# to settings file
|
||||
|
||||
from os import path
|
||||
|
||||
class inkycal_settings:
|
||||
"""Load and validate settings from the settings file"""
|
||||
|
||||
__supported_languages = ['en', 'de', 'ru', 'it', 'es', 'fr', 'el', 'sv', 'nl',
|
||||
'pl', 'ua', 'nb', 'vi', 'zh_tw', 'zh-cn', 'ja', 'ko']
|
||||
__supported_units = ['metric', 'imperial']
|
||||
__supported_hours = [12, 24]
|
||||
__supported_display_orientation = ['normal', 'upside_down']
|
||||
__supported_models = [
|
||||
'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',
|
||||
'epd_4_in_2_colour', 'epd_4_in_2'
|
||||
]
|
||||
|
||||
def __init__(self, settings_file_path):
|
||||
"""Load settings from path (folder or settings.json file)"""
|
||||
try:
|
||||
if settings_file_path.endswith('settings.json'):
|
||||
folder = settings_file_path.split('/settings.json')[0]
|
||||
else:
|
||||
folder = settings_file_path
|
||||
|
||||
chdir(folder)
|
||||
with open("settings.json") as file:
|
||||
self.raw_settings = json.load(file)
|
||||
|
||||
except FileNotFoundError:
|
||||
print('No settings file found in specified location')
|
||||
|
||||
try:
|
||||
self.language = self.raw_settings['language']
|
||||
if self.language not in self.__supported_languages or type(self.language) != str:
|
||||
print('Unsupported language: {}!. Switching to english'.format(language))
|
||||
self.language = 'en'
|
||||
|
||||
|
||||
self.units = self.raw_settings['units']
|
||||
if self.units not in self.__supported_units or type(self.units) != str:
|
||||
print('Units ({}) not supported, using metric units.'.format(units))
|
||||
self.units = 'metric'
|
||||
|
||||
|
||||
self.hours = self.raw_settings['hours']
|
||||
if self.hours not in self.__supported_hours or type(self.hours) != int:
|
||||
print('Selected hours: {} not supported, using 24-hours'.format(hours))
|
||||
self.hours = '24'
|
||||
|
||||
|
||||
self.model = self.raw_settings['model']
|
||||
if self.model not in self.__supported_models or type(self.model) != str:
|
||||
print('Model: {} not supported. Please select a valid option'.format(model))
|
||||
print('Switching to 7.5" ePaper black-white (v1) (fallback)')
|
||||
self.model = 'epd_7_in_5'
|
||||
|
||||
|
||||
self.calibration_hours = self.raw_settings['calibration_hours']
|
||||
if not self.calibration_hours or type(self.calibration_hours) != list:
|
||||
print('Invalid calibration hours: {}'.format(calibration_hours))
|
||||
print('Using default option, 0am,12am,6pm')
|
||||
self.calibration_hours = [0,12,18]
|
||||
|
||||
|
||||
self.display_orientation = self.raw_settings['display_orientation']
|
||||
if self.display_orientation not in self.__supported_display_orientation or type(
|
||||
self.display_orientation) != str:
|
||||
print('Invalid ({}) display orientation.'.format(display_orientation))
|
||||
print('Switching to default orientation, normal-mode')
|
||||
self.display_orientation = 'normal'
|
||||
|
||||
### Check if empty, If empty, set to none
|
||||
for sections in self.raw_settings['panels']:
|
||||
|
||||
if sections['location'] == 'top':
|
||||
self.top_section = sections['type']
|
||||
self.top_section_config = sections['config']
|
||||
|
||||
elif sections['location'] == 'middle':
|
||||
self.middle_section = sections['type']
|
||||
self.middle_section_config = sections['config']
|
||||
|
||||
elif sections['location'] == 'bottom':
|
||||
self.bottom_section = sections['type']
|
||||
self.bottom_section_config = sections['config']
|
||||
|
||||
|
||||
print('settings loaded')
|
||||
except Exception as e:
|
||||
print(e.reason)
|
||||
|
||||
def module_init(self, module_name):
|
||||
"""Get all data from settings file by providing the module name"""
|
||||
if module_name == self.top_section:
|
||||
config = self.top_section_config
|
||||
elif module_name == self.middle_section:
|
||||
config = self.middle_section_config
|
||||
elif module_name == self.bottom_section:
|
||||
config = self.bottom_section_config
|
||||
else:
|
||||
print('Invalid module name!')
|
||||
config = None
|
||||
|
||||
for module in self.raw_settings['panels']:
|
||||
if module_name == module['type']:
|
||||
location = module['location']
|
||||
|
||||
return config, location
|
||||
|
||||
def which_modules(self):
|
||||
"""Returns a list of modules (from settings file) which should be loaded
|
||||
on start"""
|
||||
lst = [self.top_section, self.middle_section, self.bottom_section]
|
||||
return lst
|
||||
|
||||
|
||||
def main():
|
||||
print('running settings parser as standalone...')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
2
inkycal/display/__init__.py
Normal file
2
inkycal/display/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .layout import inkycal_layout
|
||||
print('imported layout class')
|
82
inkycal/display/layout.py
Normal file
82
inkycal/display/layout.py
Normal file
@ -0,0 +1,82 @@
|
||||
class inkycal_layout:
|
||||
"""Page layout handling"""
|
||||
|
||||
def __init__(self, model=None, width=None, height=None,
|
||||
supports_colour=False):
|
||||
"""Initialize parameters for specified epaper model
|
||||
Use model parameter to specify display OR
|
||||
Crate a custom display with given width and height"""
|
||||
|
||||
self.background_colour = 'white' # Move to inkycal_rendering
|
||||
self.text_colour = 'black' # Move to inkycal_rendering
|
||||
|
||||
if (model != None) and (width == None) and (height == None):
|
||||
display_dimensions = {
|
||||
'epd_7_in_5_v2_colour': (800, 400),
|
||||
'epd_7_in_5_v2': (800, 400),
|
||||
'epd_7_in_5_colour': (640, 384),
|
||||
'epd_7_in_5': (640, 384),
|
||||
'epd_5_in_83_colour': (600, 448),
|
||||
'epd_5_in_83': (600, 448),
|
||||
'epd_4_in_2_colour': (400, 300),
|
||||
'epd_4_in_2': (400, 300),
|
||||
}
|
||||
|
||||
self.display_height, self.display_width = display_dimensions[model]
|
||||
if 'colour' in model:
|
||||
self.three_colour_support = True
|
||||
|
||||
elif width and height:
|
||||
self.display_height = width
|
||||
self.display_width = height
|
||||
self.supports_colour = supports_colour
|
||||
|
||||
else:
|
||||
print("Can't create a layout without given sizes")
|
||||
raise
|
||||
|
||||
self.__top_section_width = self.display_width
|
||||
self.__middle_section_width = self.display_width
|
||||
self.__bottom_section_width = self.display_width
|
||||
self.create_sections()
|
||||
|
||||
def create_sections(self, top_section=0.10, middle_section=0.65,
|
||||
bottom_section=0.25):
|
||||
"""Allocate fixed percentage height for top and middle section
|
||||
e.g. 0.2 = 20% (Leave empty for default values)
|
||||
Set top/bottom_section to 0 to allocate more space for the middle section
|
||||
"""
|
||||
scale = lambda percentage: round(percentage * self.display_height)
|
||||
|
||||
if top_section == 0 or bottom_section == 0:
|
||||
if top_section == 0:
|
||||
self.__top_section_height = 0
|
||||
|
||||
if bottom_section == 0:
|
||||
self.__bottom_section_height = 0
|
||||
|
||||
self.__middle_section_height = scale(1 - top_section - bottom_section)
|
||||
else:
|
||||
if top_section + middle_section + bottom_section > 1.0:
|
||||
print('All percentages should add up to max 100%, not more!')
|
||||
raise
|
||||
|
||||
self.__top_section_height = scale(top_section)
|
||||
self.__middle_section_height = scale(middle_section)
|
||||
self.__bottom_section_height = (self.display_height -
|
||||
self.__top_section_height - self.__middle_section_height)
|
||||
|
||||
def get_section_size(self, section):
|
||||
"""Enter top/middle/bottom to get the size of the section as a tuple:
|
||||
(width, height)"""
|
||||
if section not in ['top','middle','bottom']:
|
||||
print('Invalid section: ', section)
|
||||
raise
|
||||
else:
|
||||
if section == 'top':
|
||||
size = (self.__top_section_width, self.__top_section_height)
|
||||
elif section == 'middle':
|
||||
size = (self.__middle_section_width, self.__middle_section_height)
|
||||
elif section == 'bottom':
|
||||
size = (self.__bottom_section_width, self.__bottom_section_height)
|
||||
return size
|
0
inkycal/display/operations.py
Normal file
0
inkycal/display/operations.py
Normal file
0
inkycal/modules/__init__.py
Normal file
0
inkycal/modules/__init__.py
Normal file
140
inkycal/modules/inkycal_rss.py
Normal file
140
inkycal/modules/inkycal_rss.py
Normal file
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
RSS module for Inky-Calendar Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
|
||||
from inkycal.render.functions import *
|
||||
from random import shuffle
|
||||
|
||||
try:
|
||||
import feedparser
|
||||
except ImportError:
|
||||
print('feedparser is not installed! Please install with:')
|
||||
print('pipe install feedparser')
|
||||
|
||||
|
||||
# Debug Data
|
||||
size = (384, 160)
|
||||
config = {'rss_urls': ['http://feeds.bbci.co.uk/news/world/rss.xml#']}
|
||||
|
||||
|
||||
class inkycal_rss:
|
||||
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
self.name = os.path.basename(__file__).split('.py')[0]
|
||||
self.config = section_config
|
||||
self.width, self.height = section_size
|
||||
|
||||
self.background_colour = 'white'
|
||||
self.font_colour = 'black'
|
||||
self.fontsize = 12
|
||||
self.font = ImageFont.truetype(
|
||||
fonts['NotoSans-SemiCondensed'], size = self.fontsize)
|
||||
self.padding_x = 0.02
|
||||
self.padding_y = 0.05
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""Manually set some parameters of this module"""
|
||||
for key, value in kwargs.items():
|
||||
if key in self.__dict__:
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
print('{0} does not exist'.format(key))
|
||||
pass
|
||||
|
||||
def get(self, **kwargs):
|
||||
"""Manually get some parameters of this module"""
|
||||
for key, value in kwargs.items():
|
||||
if key in self.__dict__:
|
||||
getattr(self, key, value)
|
||||
else:
|
||||
print('{0} does not exist'.format(key))
|
||||
pass
|
||||
|
||||
def get_options(self):
|
||||
"""Get all options which can be changed"""
|
||||
return self.__dict__
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = int(self.width - (self.width * 2 * self.padding_x))
|
||||
im_height = int(self.height - (self.height * 2 * self.padding_y))
|
||||
im_size = im_width, im_height
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size = im_size, color = self.background_colour)
|
||||
im_colour = Image.new('RGB', size = im_size, color = 'white')
|
||||
|
||||
# Set some parameters for formatting rss feeds
|
||||
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)]
|
||||
|
||||
if internet_available() == True:
|
||||
print('Connection test passed')
|
||||
else:
|
||||
# write 'No network available :('
|
||||
raise Exception('Network could not be reached :(')
|
||||
|
||||
try:
|
||||
# Create list containing all rss-feeds from all rss-feed urls
|
||||
parsed_feeds = []
|
||||
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))
|
||||
# print(parsed_feeds)
|
||||
|
||||
# Shuffle the list to prevent showing the same content
|
||||
shuffle(parsed_feeds)
|
||||
|
||||
# Trim down the list to the max number of lines
|
||||
del parsed_feeds[max_lines:]
|
||||
|
||||
|
||||
# Wrap long text from feeds (line-breaking)
|
||||
flatten = lambda z: [x for y in z for x in y]
|
||||
filtered_feeds, counter = [], 0
|
||||
|
||||
for posts in parsed_feeds:
|
||||
wrapped = text_wrap(posts, font = self.font, max_width = line_width)
|
||||
counter += len(filtered_feeds) + len(wrapped)
|
||||
if counter < max_lines:
|
||||
filtered_feeds.append(wrapped)
|
||||
filtered_feeds = flatten(filtered_feeds)
|
||||
|
||||
# Write rss-feeds on image
|
||||
"""Write the correctly formatted text on the display"""
|
||||
for _ in range(len(filtered_feeds)):
|
||||
write(im_black, line_positions[_], (line_width, line_height),
|
||||
filtered_feeds[_], font = self.font, alignment= 'left')
|
||||
|
||||
# Cleanup
|
||||
del filtered_feeds, parsed_feeds, wrapped, counter, text
|
||||
|
||||
except Exception as e:
|
||||
print('Error in {0}'.format(self.name))
|
||||
print('Reason: ',e)
|
||||
write(im_black, (0,0), (im_width, im_height), str(e), font = self.font)
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png')
|
||||
im_colour.save(images+self.name+'_colour.png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone mode'.format(
|
||||
os.path.basename(__file__).split('.py')[0]))
|
1
inkycal/render/__init__.py
Normal file
1
inkycal/render/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .fonts import inkycal_fonts
|
165
inkycal/render/functions.py
Normal file
165
inkycal/render/functions.py
Normal file
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Inky-Calendar custom-functions for ease-of-use
|
||||
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageColor
|
||||
from urllib.request import urlopen
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
##from glob import glob
|
||||
##import importlib
|
||||
##import subprocess as subp
|
||||
##import numpy
|
||||
##import arrow
|
||||
##from pytz import timezone
|
||||
|
||||
|
||||
|
||||
##"""Set some display parameters"""
|
||||
##driver = importlib.import_module('drivers.'+model)
|
||||
|
||||
# Get the path to the Inky-Calendar folder
|
||||
top_level = os.path.dirname(
|
||||
os.path.abspath(os.path.dirname(__file__))).split('/inkycal')[0]
|
||||
|
||||
# Get path of 'fonts' and 'images' folders within Inky-Calendar folder
|
||||
fonts_location = top_level + '/fonts/'
|
||||
images = top_level + '/images/'
|
||||
|
||||
# Get available fonts within fonts folder
|
||||
fonts = {}
|
||||
|
||||
for path,dirs,files in os.walk(fonts_location):
|
||||
for filename in files:
|
||||
if filename.endswith('.otf'):
|
||||
name = filename.split('.otf')[0]
|
||||
fonts[name] = os.path.join(path, filename)
|
||||
|
||||
if filename.endswith('.ttf'):
|
||||
name = filename.split('.ttf')[0]
|
||||
fonts[name] = os.path.join(path, filename)
|
||||
|
||||
del name, filename, files
|
||||
|
||||
available_fonts = [key for key,values in fonts.items()]
|
||||
|
||||
def get_fonts():
|
||||
"""Print all available fonts by name"""
|
||||
for fonts in available_fonts:
|
||||
print(fonts)
|
||||
|
||||
def write(image, xy, box_size, text, font=None, **kwargs):
|
||||
"""Write text on specified image
|
||||
image = on which image should the text be added?
|
||||
xy = xy-coordinates as tuple -> (x,y)
|
||||
box_size = size of text-box -> (width,height)
|
||||
text = string (what to write)
|
||||
font = which font to use
|
||||
"""
|
||||
|
||||
allowed_kwargs = ['alignment', 'autofit', 'colour', 'rotation'
|
||||
'fill_width', 'fill_height']
|
||||
alignment='center'
|
||||
autofit = False
|
||||
fill_width = 1.0
|
||||
fill_height = 0.8
|
||||
colour = 'black'
|
||||
rotation = None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key in allowed_kwargs:
|
||||
setattr(write, key, value)
|
||||
else:
|
||||
print('{0} does not exist'.format(key))
|
||||
pass
|
||||
|
||||
x,y = xy
|
||||
box_width, box_height = box_size
|
||||
|
||||
# Increase fontsize to fit specified height and width of text box
|
||||
if autofit == True or fill_width != 1.0 or fill_height != 0.8:
|
||||
size = 8
|
||||
font = ImageFont.truetype(font, size)
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
while (
|
||||
text_width < int(box_width * fill_width)
|
||||
) and (
|
||||
text_height < int(box_height * fill_height)
|
||||
):
|
||||
size += 1
|
||||
font = ImageFont.truetype(font, size)
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
|
||||
# Truncate text if text is too long so it can fit inside the box
|
||||
while (text_width, text_height) > (box_width, box_height):
|
||||
text=text[0:-1]
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
|
||||
# Align text to desired position
|
||||
if alignment == "" or "center" or None:
|
||||
x = int((box_width / 2) - (text_width / 2))
|
||||
elif alignment == 'left':
|
||||
x = 0
|
||||
elif alignment == 'right':
|
||||
x = int(box_width - text_width)
|
||||
|
||||
y = int((box_height / 2) - (text_height / 2))
|
||||
|
||||
# Draw the text in the text-box
|
||||
draw = ImageDraw.Draw(image)
|
||||
space = Image.new('RGBA', (box_width, box_height))
|
||||
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
|
||||
|
||||
if rotation != None:
|
||||
space.rotate(rotation, expand = True)
|
||||
|
||||
# Update only region with text (add text with transparent background)
|
||||
image.paste(space, xy, space)
|
||||
|
||||
|
||||
|
||||
def text_wrap(text, font=None, max_width = None):
|
||||
"""Split long text (text-wrapping). Returns a list"""
|
||||
lines = []
|
||||
if font.getsize(text)[0] < max_width:
|
||||
lines.append(text)
|
||||
else:
|
||||
words = text.split(' ')
|
||||
i = 0
|
||||
while i < len(words):
|
||||
line = ''
|
||||
while i < len(words) and font.getsize(line + words[i])[0] <= max_width:
|
||||
line = line + words[i] + " "
|
||||
i += 1
|
||||
if not line:
|
||||
line = words[i]
|
||||
i += 1
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def internet_available():
|
||||
"""check if the internet is available"""
|
||||
try:
|
||||
urlopen('https://google.com',timeout=5)
|
||||
return True
|
||||
except URLError as err:
|
||||
return False
|
||||
|
||||
|
||||
def get_system_tz():
|
||||
"""Get the timezone set by the system"""
|
||||
try:
|
||||
local_tz = time.tzname[1]
|
||||
except:
|
||||
print('System timezone could not be parsed!')
|
||||
print('Please set timezone manually!. Setting timezone to None...')
|
||||
local_tz = None
|
||||
return local_tz
|
@ -1 +0,0 @@
|
||||
This is just a dummy file.
|
@ -1 +0,0 @@
|
||||
#nothing in here. What did you expect?
|
@ -1,173 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
v1.7.2
|
||||
|
||||
Main file of Inky-Calendar software. Creates dynamic images for each section,
|
||||
assembles them and sends it to the E-Paper
|
||||
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from configuration import *
|
||||
import arrow
|
||||
from time import sleep
|
||||
import gc
|
||||
|
||||
"""Perepare for execution of main programm"""
|
||||
calibration_countdown = 'initial'
|
||||
skip_calibration = False
|
||||
upside_down = False
|
||||
|
||||
image_cleanup()
|
||||
|
||||
try:
|
||||
top_section_module = importlib.import_module(top_section)
|
||||
except ValueError:
|
||||
print('Something went wrong while importing the top-section module:', top_section)
|
||||
pass
|
||||
|
||||
try:
|
||||
middle_section_module = importlib.import_module(middle_section)
|
||||
except ValueError:
|
||||
print('Something went wrong while importing the middle_section module', middle_section)
|
||||
pass
|
||||
|
||||
try:
|
||||
bottom_section_module = importlib.import_module(bottom_section)
|
||||
except ValueError:
|
||||
print('Something went wrong while importing the bottom_section module', bottom_section)
|
||||
pass
|
||||
|
||||
"""Check time and calibrate display if time """
|
||||
while True:
|
||||
now = arrow.now(tz=get_tz())
|
||||
for _ in range(1):
|
||||
image = Image.new('RGB', (display_width, display_height), background_colour)
|
||||
if three_colour_support == True:
|
||||
image_col = Image.new('RGB', (display_width, display_height), 'white')
|
||||
|
||||
"""------------------Add short info------------------"""
|
||||
print('Current Date: {0} \nCurrent Time: {1}'.format(now.format(
|
||||
'D MMM YYYY'), now.format('HH:mm')))
|
||||
print('-----------Main programm started now----------')
|
||||
|
||||
"""------------------Calibration check----------------"""
|
||||
if skip_calibration != True:
|
||||
print('Calibration..', end = ' ')
|
||||
if now.hour in calibration_hours:
|
||||
if calibration_countdown == 'initial':
|
||||
print('required. Performing calibration now.')
|
||||
calibration_countdown = 0
|
||||
calibrate_display(3)
|
||||
else:
|
||||
if calibration_countdown % (60 // int(update_interval)) == 0:
|
||||
calibrate_display(3)
|
||||
calibration_countdown = 0
|
||||
else:
|
||||
print('not required. Continuing...')
|
||||
else:
|
||||
print('Calibration skipped!. Please note that not calibrating e-paper',
|
||||
'displays causes ghosting')
|
||||
|
||||
|
||||
"""----------------------top-section-image-----------------------------"""
|
||||
try:
|
||||
top_section_module.main()
|
||||
top_section_image = Image.open(image_path + top_section+'.png')
|
||||
image.paste(top_section_image, (0, 0))
|
||||
|
||||
if three_colour_support == True:
|
||||
top_section_image_col = Image.open(image_path + top_section+'_col.png')
|
||||
image_col.paste(top_section_image_col, (0, 0))
|
||||
|
||||
except Exception as error:
|
||||
print(error)
|
||||
pass
|
||||
|
||||
"""----------------------middle-section-image---------------------------"""
|
||||
try:
|
||||
middle_section_module.main()
|
||||
middle_section_image = Image.open(image_path + middle_section+'.png')
|
||||
image.paste(middle_section_image, (0, middle_section_offset))
|
||||
|
||||
if three_colour_support == True:
|
||||
middle_section_image_col = Image.open(image_path + middle_section+'_col.png')
|
||||
image_col.paste(middle_section_image_col, (0, middle_section_offset))
|
||||
|
||||
except Exception as error:
|
||||
print(error)
|
||||
pass
|
||||
|
||||
|
||||
"""----------------------bottom-section-image---------------------------"""
|
||||
try:
|
||||
bottom_section_module.main()
|
||||
bottom_section_image = Image.open(image_path + bottom_section+'.png')
|
||||
image.paste(bottom_section_image, (0, bottom_section_offset))
|
||||
|
||||
if three_colour_support == True:
|
||||
bottom_section_image_col = Image.open(image_path + bottom_section+'_col.png')
|
||||
image_col.paste(bottom_section_image_col, (0, bottom_section_offset))
|
||||
|
||||
except Exception as error:
|
||||
print(error)
|
||||
pass
|
||||
|
||||
"""---------------------------------------------------------------------"""
|
||||
if upside_down == True:
|
||||
image = image.rotate(180, expand=True)
|
||||
if three_colour_support == True:
|
||||
image_col = image_col.rotate(180, expand=True)
|
||||
|
||||
image = optimise_colours(image)
|
||||
image.save(image_path + 'canvas.png')
|
||||
|
||||
if three_colour_support == True:
|
||||
image_col = optimise_colours(image_col)
|
||||
image_col.save(image_path+'canvas_col.png')
|
||||
|
||||
"""---------Refreshing E-Paper with newly created image-----------"""
|
||||
epaper = driver.EPD()
|
||||
print('Initialising E-Paper...', end = '')
|
||||
epaper.init()
|
||||
print('Done')
|
||||
|
||||
if three_colour_support == True:
|
||||
print('Sending image data and refreshing display...', end='')
|
||||
epaper.display(epaper.getbuffer(image), epaper.getbuffer(image_col))
|
||||
print('Done')
|
||||
else:
|
||||
print('Sending image data and refreshing display...', end='')
|
||||
epaper.display(epaper.getbuffer(image))
|
||||
print('Done')
|
||||
|
||||
print('Sending E-Paper to deep sleep...', end = '')
|
||||
epaper.sleep()
|
||||
print('Done')
|
||||
|
||||
"""--------------Post processing after main loop-----------------"""
|
||||
"""Collect some garbage to free up some resources"""
|
||||
gc.collect()
|
||||
|
||||
"""Adjust calibration countdowns"""
|
||||
if calibration_countdown == 'initial':
|
||||
calibration_countdown = 0
|
||||
calibration_countdown += 1
|
||||
|
||||
"""Calculate duration until next display refresh"""
|
||||
for _ in range(1):
|
||||
update_timings = [(60 - int(update_interval)*updates) for updates in
|
||||
range(60//int(update_interval))][::-1]
|
||||
|
||||
for _ in update_timings:
|
||||
if now.minute <= _:
|
||||
minutes = _ - now.minute
|
||||
break
|
||||
|
||||
refresh_countdown = minutes*60 + (60 - now.second)
|
||||
print('{0} Minutes left until next refresh'.format(minutes))
|
||||
|
||||
del update_timings, minutes, image
|
||||
image_cleanup()
|
||||
sleep(refresh_countdown)
|
@ -1,156 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Agenda module for Inky-Calendar Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from inkycal_icalendar import fetch_events
|
||||
from configuration import*
|
||||
|
||||
show_events = True
|
||||
print_events = False
|
||||
style = 'D MMM YY HH:mm'
|
||||
all_day_str = 'All day'
|
||||
|
||||
"""Add a border to increase readability"""
|
||||
border_top = int(middle_section_height * 0.02)
|
||||
border_left = int(middle_section_width * 0.02)
|
||||
|
||||
"""Choose font optimised for the agenda section"""
|
||||
font = ImageFont.truetype(NotoSans+'Medium.ttf', agenda_fontsize)
|
||||
line_height = int(font.getsize('hg')[1] * 1.2) + 1
|
||||
line_width = int(middle_section_width - (border_left*2))
|
||||
|
||||
"""Set some positions for events, dates and times"""
|
||||
date_col_width = int(line_width * 0.20)
|
||||
time_col_width = int(line_width * 0.15)
|
||||
event_col_width = int(line_width - date_col_width - time_col_width)
|
||||
|
||||
date_col_start = border_left
|
||||
time_col_start = date_col_start + date_col_width
|
||||
event_col_start = time_col_start + time_col_width
|
||||
|
||||
"""Find max number of lines that can fit in the middle section and allocate
|
||||
a position for each line"""
|
||||
max_lines = int(middle_section_height+bottom_section_height -
|
||||
(border_top * 2))// line_height
|
||||
|
||||
line_pos = [(border_left, int(top_section_height + border_top + line * line_height))
|
||||
for line in range(max_lines)]
|
||||
|
||||
def generate_image():
|
||||
if middle_section == 'inkycal_agenda' and internet_available() == True:
|
||||
try:
|
||||
clear_image('middle_section')
|
||||
if not bottom_section:
|
||||
clear_image('bottom_section')
|
||||
|
||||
print('Agenda module: Generating image...', end = '')
|
||||
now = arrow.now(get_tz())
|
||||
today_start = arrow.get(now.year, now.month, now.day)
|
||||
|
||||
"""Create a list of dictionaries containing dates of the next days"""
|
||||
agenda_events = [{'date':today_start.replace(days=+_),
|
||||
'date_str': now.replace(days=+_).format('ddd D MMM',locale=language),
|
||||
'type':'date'} for _ in range(max_lines)]
|
||||
|
||||
"""Copy the list from the icalendar module with some conditions"""
|
||||
upcoming_events = fetch_events()
|
||||
filtered_events = [events for events in upcoming_events if
|
||||
events.end > now]
|
||||
|
||||
"""Set print_events_to True to print all events in this month"""
|
||||
if print_events == True and filtered_events:
|
||||
auto_line_width = max(len(_.name) for _ in filtered_events)
|
||||
for events in filtered_events:
|
||||
print('{0} {1} | {2} | {3} | All day ='.format(events.name,
|
||||
' '* (auto_line_width - len(events.name)), events.begin.format(style),
|
||||
events.end.format(style)), events.all_day)
|
||||
|
||||
"""Convert the event-timings from utc to the specified locale's time
|
||||
and create a ready-to-display list for the agenda view"""
|
||||
for events in filtered_events:
|
||||
if not events.all_day:
|
||||
agenda_events.append({'date': events.begin, 'time': events.begin.format(
|
||||
'HH:mm' if hours == '24' else 'hh:mm a'), 'name':str(events.name),
|
||||
'type':'timed_event'})
|
||||
else:
|
||||
if events.duration.days == 1:
|
||||
agenda_events.append({'date': events.begin,'time': all_day_str,
|
||||
'name': events.name,'type':'full_day_event'})
|
||||
else:
|
||||
for day in range(events.duration.days):
|
||||
agenda_events.append({'date': events.begin.replace(days=+day),
|
||||
'time': all_day_str,'name':events.name, 'type':'full_day_event'})
|
||||
|
||||
"""Sort events and dates in chronological order"""
|
||||
agenda_events = sorted(agenda_events, key = lambda event: event['date'])
|
||||
|
||||
"""Crop the agenda_events in case it's too long"""
|
||||
del agenda_events[max_lines:]
|
||||
|
||||
"""Display all events, dates and times on the display"""
|
||||
if show_events == True:
|
||||
previous_date = None
|
||||
for events in range(len(agenda_events)):
|
||||
if agenda_events[events]['type'] == 'date':
|
||||
if previous_date == None or previous_date != agenda_events[events][
|
||||
'date']:
|
||||
write_text(date_col_width, line_height,
|
||||
agenda_events[events]['date_str'], line_pos[events], font = font)
|
||||
|
||||
previous_date = agenda_events[events]['date']
|
||||
|
||||
if three_colour_support == True:
|
||||
draw_col.line((date_col_start, line_pos[events][1],
|
||||
line_width,line_pos[events][1]), fill = 'black')
|
||||
else:
|
||||
draw.line((date_col_start, line_pos[events][1],
|
||||
line_width,line_pos[events][1]), fill = 'black')
|
||||
|
||||
|
||||
elif agenda_events[events]['type'] == 'timed_event':
|
||||
write_text(time_col_width, line_height, agenda_events[events]['time'],
|
||||
(time_col_start, line_pos[events][1]), font = font)
|
||||
|
||||
write_text(event_col_width, line_height, ('• '+agenda_events[events][
|
||||
'name']), (event_col_start, line_pos[events][1]),
|
||||
alignment = 'left', font = font)
|
||||
|
||||
else:
|
||||
write_text(time_col_width, line_height, agenda_events[events]['time'],
|
||||
(time_col_start, line_pos[events][1]), font = font)
|
||||
|
||||
write_text(event_col_width, line_height, ('• '+agenda_events[events]['name']),
|
||||
(event_col_start, line_pos[events][1]), alignment = 'left', font = font)
|
||||
|
||||
"""Crop the image to show only the middle section"""
|
||||
agenda_image = image.crop((0,middle_section_offset,display_width, display_height))
|
||||
agenda_image.save(image_path+'inkycal_agenda.png')
|
||||
|
||||
if three_colour_support == True:
|
||||
agenda_image_col = image_col.crop((0,middle_section_offset,display_width, display_height))
|
||||
agenda_image_col.save(image_path+'inkycal_agenda_col.png')
|
||||
|
||||
print('Done')
|
||||
|
||||
except Exception as e:
|
||||
"""If something went wrong, print a Error message on the Terminal"""
|
||||
print('Failed!')
|
||||
print('Error in Agenda module!')
|
||||
print('Reason: ',e)
|
||||
|
||||
clear_image('middle_section')
|
||||
write_text(middle_section_width, middle_section_height, str(e),
|
||||
(0, middle_section_offset), font = font)
|
||||
calendar_image = crop_image(image, 'middle_section')
|
||||
calendar_image.save(image_path+'inkycal_agenda.png')
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
generate_image()
|
||||
|
||||
main()
|
@ -1,232 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Calendar module for Inky-Calendar Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import calendar
|
||||
from configuration import *
|
||||
|
||||
print_events = False
|
||||
show_events = True
|
||||
today_in_your_language = 'today'
|
||||
tomorrow_in_your_language = 'tomorrow'
|
||||
at_in_your_language = 'at'
|
||||
event_icon = 'square' # dot #square
|
||||
style = "DD MMM"
|
||||
|
||||
font = ImageFont.truetype(NotoSans+'.ttf', calendar_fontsize)
|
||||
space_between_lines = 0
|
||||
|
||||
if show_events == True:
|
||||
from inkycal_icalendar import fetch_events
|
||||
|
||||
"""Add a border to increase readability"""
|
||||
border_top = int(middle_section_height * 0.02)
|
||||
border_left = int(middle_section_width * 0.02)
|
||||
|
||||
main_area_height = middle_section_height-border_top*2
|
||||
main_area_width = middle_section_width-border_left*2
|
||||
|
||||
line_height = font.getsize('hg')[1] + space_between_lines
|
||||
line_width = middle_section_width - (border_left*2)
|
||||
|
||||
"""Calculate height for each sub-section"""
|
||||
month_name_height = int(main_area_height*0.1)
|
||||
weekdays_height = int(main_area_height*0.05)
|
||||
calendar_height = int(main_area_height*0.6)
|
||||
events_height = int(main_area_height*0.25)
|
||||
|
||||
"""Set rows and coloumns in the calendar section and calculate sizes"""
|
||||
calendar_rows, calendar_coloumns = 6, 7
|
||||
icon_width = main_area_width // calendar_coloumns
|
||||
icon_height = calendar_height // calendar_rows
|
||||
|
||||
"""Calculate paddings for calendar section"""
|
||||
x_padding_calendar = int((main_area_width % icon_width) / 2)
|
||||
y_padding_calendar = int((main_area_height % calendar_rows) / 2)
|
||||
|
||||
"""Add coordinates for number icons inside the calendar section"""
|
||||
grid_start_y = (middle_section_offset + border_top + month_name_height +
|
||||
weekdays_height + y_padding_calendar)
|
||||
grid_start_x = border_left + x_padding_calendar
|
||||
|
||||
grid = [(grid_start_x + icon_width*x, grid_start_y + icon_height*y)
|
||||
for y in range(calendar_rows) for x in range(calendar_coloumns)]
|
||||
|
||||
weekday_pos = [(grid_start_x + icon_width*_, middle_section_offset +
|
||||
month_name_height) for _ in range(calendar_coloumns)]
|
||||
|
||||
max_event_lines = (events_height - border_top) // (font.getsize('hg')[1]
|
||||
+ space_between_lines)
|
||||
|
||||
event_lines = [(border_left,(bottom_section_offset - events_height)+
|
||||
int(events_height/max_event_lines*_)) for _ in range(max_event_lines)]
|
||||
|
||||
def generate_image():
|
||||
if middle_section == "inkycal_calendar" and internet_available() == True:
|
||||
try:
|
||||
clear_image('middle_section')
|
||||
print('Calendar module: Generating image...', end = '')
|
||||
now = arrow.now(tz = get_tz())
|
||||
|
||||
"""Set up the Calendar template based on personal preferences"""
|
||||
if week_starts_on == "Monday":
|
||||
calendar.setfirstweekday(calendar.MONDAY)
|
||||
weekstart = now.replace(days = - now.weekday())
|
||||
else:
|
||||
calendar.setfirstweekday(calendar.SUNDAY)
|
||||
weekstart = now.replace(days = - now.isoweekday())
|
||||
|
||||
"""Write the name of the current month at the correct position"""
|
||||
write_text(main_area_width, month_name_height,
|
||||
str(now.format('MMMM',locale=language)), (border_left,
|
||||
middle_section_offset), autofit = True)
|
||||
|
||||
"""Set up weeknames in local language and add to main section"""
|
||||
weekday_names = [weekstart.replace(days=+_).format('ddd',locale=language)
|
||||
for _ in range(7)]
|
||||
|
||||
for _ in range(len(weekday_pos)):
|
||||
write_text(icon_width, weekdays_height, weekday_names[_],
|
||||
weekday_pos[_], autofit = True)
|
||||
|
||||
"""Create a calendar template and flatten (remove nestings)"""
|
||||
flatten = lambda z: [x for y in z for x in y]
|
||||
calendar_flat = flatten(calendar.monthcalendar(now.year, now.month))
|
||||
|
||||
"""Add the numbers on the correct positions"""
|
||||
for i in range(len(calendar_flat)):
|
||||
if calendar_flat[i] not in (0, int(now.day)):
|
||||
write_text(icon_width, icon_height, str(calendar_flat[i]), grid[i])
|
||||
|
||||
"""Draw a red/black circle with the current day of month in white"""
|
||||
icon = Image.new('RGBA', (icon_width, icon_height))
|
||||
current_day_pos = grid[calendar_flat.index(now.day)]
|
||||
x_circle,y_circle = int(icon_width/2), int(icon_height/2)
|
||||
radius = int(icon_width * 0.25)
|
||||
text_width, text_height = default.getsize(str(now.day))
|
||||
x_text = int((icon_width / 2) - (text_width / 2))
|
||||
y_text = int((icon_height / 2) - (text_height / 1.7))
|
||||
ImageDraw.Draw(icon).ellipse((x_circle-radius, y_circle-radius,
|
||||
x_circle+radius, y_circle+radius), fill= 'black', outline=None)
|
||||
ImageDraw.Draw(icon).text((x_text, y_text), str(now.day), fill='white',
|
||||
font=bold)
|
||||
if three_colour_support == True:
|
||||
image_col.paste(icon, current_day_pos, icon)
|
||||
else:
|
||||
image.paste(icon, current_day_pos, icon)
|
||||
|
||||
"""Create some reference points for the current month"""
|
||||
days_current_month = calendar.monthrange(now.year, now.month)[1]
|
||||
month_start = now.floor('month')
|
||||
month_end = now.ceil('month')
|
||||
|
||||
if show_events == True:
|
||||
"""Filter events which begin before the end of this month"""
|
||||
upcoming_events = fetch_events()
|
||||
|
||||
calendar_events = [events for events in upcoming_events if
|
||||
month_start <= events.end <= month_end ]
|
||||
|
||||
"""Find days with events in the current month"""
|
||||
days_with_events = []
|
||||
for events in calendar_events:
|
||||
if events.duration.days <= 1:
|
||||
days_with_events.append(int(events.begin.format('D')))
|
||||
else:
|
||||
for day in range(events.duration.days):
|
||||
days_with_events.append(
|
||||
int(events.begin.replace(days=+i).format('D')))
|
||||
days_with_events = set(days_with_events)
|
||||
|
||||
if event_icon == 'dot':
|
||||
for days in days_with_events:
|
||||
write_text(icon_width, int(icon_height * 0.2), '•',
|
||||
(grid[calendar_flat.index(days)][0],
|
||||
int(grid[calendar_flat.index(days)][1] + icon_height*0.8)))
|
||||
|
||||
if event_icon == 'square':
|
||||
square_size = int(icon_width * 0.6)
|
||||
center_x = int((icon_width - square_size) / 2)
|
||||
center_y = int((icon_height - square_size) / 2)
|
||||
for days in days_with_events:
|
||||
draw_square((int(grid[calendar_flat.index(days)][0]+center_x),
|
||||
int(grid[calendar_flat.index(days)][1] + center_y )),
|
||||
8, square_size , square_size, colour='black')
|
||||
|
||||
|
||||
"""Add a small section showing events of today and tomorrow"""
|
||||
event_list = []
|
||||
after_two_days = now.replace(days=2).floor('day')
|
||||
|
||||
for event in calendar_events:
|
||||
if event.begin.day == now.day and now < event.end:
|
||||
if event.all_day:
|
||||
event_list.append('{}: {}'.format(today_in_your_language, event.name))
|
||||
else:
|
||||
event_list.append('{0} {1} {2} : {3}'.format(today_in_your_language,
|
||||
at_in_your_language, event.begin.format('HH:mm' if hours == '24' else
|
||||
'hh:mm a'), event.name))
|
||||
|
||||
elif event.begin.day == now.replace(days=1).day:
|
||||
if event.all_day:
|
||||
event_list.append('{}: {}'.format(tomorrow_in_your_language, event.name))
|
||||
else:
|
||||
event_list.append('{0} {1} {2} : {3}'.format(tomorrow_in_your_language,
|
||||
at_in_your_language, event.begin.format('HH:mm' if hours == '24' else
|
||||
'hh:mm a'), event.name))
|
||||
|
||||
elif event.begin > after_two_days:
|
||||
if event.all_day:
|
||||
event_list.append('{}: {}'.format(event.begin.format('D MMM'), event.name))
|
||||
else:
|
||||
event_list.append('{0} {1} {2} : {3}'.format(event.begin.format('D MMM'),
|
||||
at_in_your_language, event.begin.format('HH:mm' if hours == '24' else
|
||||
'hh:mm a'), event.name))
|
||||
|
||||
del event_list[max_event_lines:]
|
||||
|
||||
if event_list:
|
||||
for lines in event_list:
|
||||
write_text(main_area_width, int(events_height/max_event_lines), lines,
|
||||
event_lines[event_list.index(lines)], font=font, alignment='left')
|
||||
else:
|
||||
write_text(main_area_width, int(events_height/max_event_lines),
|
||||
'No upcoming events.', event_lines[0], font=font, alignment='left')
|
||||
|
||||
"""Set print_events_to True to print all events in this month"""
|
||||
style = 'DD MMM YY HH:mm'
|
||||
if print_events == True and calendar_events:
|
||||
line_width = max(len(_.name) for _ in calendar_events)
|
||||
for events in calendar_events:
|
||||
print('{0} {1} | {2} | {3} | All day ='.format(events.name,
|
||||
' ' * (line_width - len(events.name)), events.begin.format(style),
|
||||
events.end.format(style)), events.all_day)
|
||||
|
||||
calendar_image = crop_image(image, 'middle_section')
|
||||
calendar_image.save(image_path+'inkycal_calendar.png')
|
||||
|
||||
if three_colour_support == True:
|
||||
calendar_image_col = crop_image(image_col, 'middle_section')
|
||||
calendar_image_col.save(image_path+'inkycal_calendar_col.png')
|
||||
|
||||
print('Done')
|
||||
|
||||
except Exception as e:
|
||||
"""If something went wrong, print a Error message on the Terminal"""
|
||||
print('Failed!')
|
||||
print('Error in Calendar module!')
|
||||
print('Reason: ',e)
|
||||
clear_image('middle_section')
|
||||
write_text(middle_section_width, middle_section_height, str(e),
|
||||
(0, middle_section_offset), font = font)
|
||||
calendar_image = crop_image(image, 'middle_section')
|
||||
calendar_image.save(image_path+'inkycal_calendar.png')
|
||||
pass
|
||||
|
||||
def main():
|
||||
generate_image()
|
||||
|
||||
main()
|
@ -1,95 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
iCalendar (parsing) module for Inky-Calendar Project
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from configuration import *
|
||||
from settings import ical_urls
|
||||
import arrow
|
||||
from ics import Calendar
|
||||
|
||||
use_recurring_events = True
|
||||
print_events = False
|
||||
style = 'DD MMM YY HH:mm'
|
||||
|
||||
|
||||
if use_recurring_events == True:
|
||||
from dateutil.rrule import rrulestr, rruleset
|
||||
import re
|
||||
|
||||
def fetch_events():
|
||||
"""Set timelines for filtering upcoming events"""
|
||||
timezone = get_tz()
|
||||
now = arrow.now(tz=timezone)
|
||||
beginning_of_month = now.replace(days= - now.day +1)
|
||||
near_future = now.replace(days= 30)
|
||||
further_future = now.replace(days=40)
|
||||
|
||||
"""Parse the iCalendars from the urls, fixing some known errors with ics"""
|
||||
calendars = [Calendar(fix_ical(url)) for url in ical_urls]
|
||||
|
||||
"""Filter any upcoming events from all iCalendars and add them to a list"""
|
||||
upcoming_events = [events for ical in calendars for events in ical.events
|
||||
if beginning_of_month <= events.end <= further_future or
|
||||
beginning_of_month <= events.begin <= near_future]
|
||||
|
||||
"""Try to parse recurring events. This is clearly experimental! """
|
||||
if use_recurring_events == True:
|
||||
for ical in calendars:
|
||||
for events in ical.events:
|
||||
event_str = str(events)
|
||||
if re.search('RRULE:(.+?)\n', event_str):
|
||||
if events.all_day and events.duration.days > 1:
|
||||
events.end = events.end.replace(days=-2)
|
||||
else:
|
||||
events.end = events.end.to(timezone)
|
||||
events.begin = events.begin.to(timezone)
|
||||
try:
|
||||
rule = re.search('RRULE:(.+?)\n', event_str).group(0)[:-2]
|
||||
if re.search('UNTIL=(.+?);', rule) and not re.search('UNTIL=(.+?)Z;', rule):
|
||||
rule = re.sub('UNTIL=(.+?);', 'UNTIL='+re.search('UNTIL=(.+?);', rule).group(0)[6:-1]+'T000000Z;', rule)
|
||||
dates = rrulestr(rule, dtstart= events.begin.datetime).between(after= now.datetime, before = further_future.datetime)
|
||||
|
||||
if dates:
|
||||
duration = events.duration
|
||||
for date in dates:
|
||||
cc = events.clone()
|
||||
cc.end = arrow.get(date+duration)
|
||||
cc.begin = arrow.get(date)
|
||||
upcoming_events.append(cc)
|
||||
#print("Added '{}' with new start at {}".format(cc.name, cc.begin.format('DD MMM YY')))
|
||||
|
||||
except Exception as e:
|
||||
print('Something went wrong while parsing recurring events')
|
||||
pass
|
||||
|
||||
"""Sort events according to their beginning date"""
|
||||
def sort_dates(event):
|
||||
return event.begin
|
||||
upcoming_events.sort(key=sort_dates)
|
||||
|
||||
"""Multiday events are displayed incorrectly; fix that"""
|
||||
for events in upcoming_events:
|
||||
if events.all_day and events.duration.days > 1:
|
||||
events.end = events.end.replace(days=-2)
|
||||
events.make_all_day()
|
||||
|
||||
if not events.all_day:
|
||||
events.end = events.end.to(timezone)
|
||||
events.begin = events.begin.to(timezone)
|
||||
|
||||
""" The list upcoming_events should not be modified. If you need the data from
|
||||
this one, copy the list or the contents to another one."""
|
||||
#print(upcoming_events) # Print all events. Might look a bit messy
|
||||
|
||||
"""Print upcoming events in a more appealing way"""
|
||||
if print_events == True and upcoming_events:
|
||||
line_width = max(len(i.name) for i in upcoming_events)
|
||||
for events in upcoming_events:
|
||||
print('{0} {1} | {2} | {3} | All day ='.format(events.name,
|
||||
' '* (line_width - len(events.name)), events.begin.format(style),
|
||||
events.end.format(style)), events.all_day)
|
||||
|
||||
return upcoming_events
|
@ -1,179 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Experimental image module for Inky-Calendar software
|
||||
Displays an image on the E-Paper. Work in progress!
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from configuration import *
|
||||
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 = True # Flip image by 180 deg (upside-down)
|
||||
alignment = 'center' # top_center, top_left, center_left, bottom_right etc.
|
||||
colours = 'bwr' # bwr # bwy # bw
|
||||
render = True # show image on E-Paper?
|
||||
"""----------------------------------------------------------------"""
|
||||
|
||||
# First determine dimensions
|
||||
if mode == 'horizontal':
|
||||
display_width, display_height == display_height, display_width
|
||||
|
||||
if mode == 'vertical':
|
||||
pass
|
||||
|
||||
# .. Then substitute possibly parameterized path
|
||||
# TODO Get (assigned) panel dimensions instead of display dimensions
|
||||
path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height))
|
||||
|
||||
"""Try to open the image if it exists and is an image file"""
|
||||
try:
|
||||
if 'http' in path:
|
||||
if path_body is None:
|
||||
# Plain GET
|
||||
im = Image.open(requests.get(path, stream=True).raw)
|
||||
else:
|
||||
# POST request, passing path_body in the body
|
||||
im = Image.open(requests.post(path, json=path_body, stream=True).raw)
|
||||
else:
|
||||
im = Image.open(path)
|
||||
except FileNotFoundError:
|
||||
print('Your file could not be found. Please check the path to your file.')
|
||||
raise
|
||||
except OSError:
|
||||
print('Please check if the path points to an image file.')
|
||||
raise
|
||||
|
||||
"""Turn image upside-down if specified"""
|
||||
if upside_down == True:
|
||||
im.rotate(180, expand = True)
|
||||
|
||||
if mode == 'auto':
|
||||
if (im.width > im.height) and (display_width < display_height):
|
||||
print('display vertical, image horizontal -> flipping image')
|
||||
im = im.rotate(90, expand=True)
|
||||
if (im.width < im.height) and (display_width > display_height):
|
||||
print('display horizontal, image vertical -> flipping image')
|
||||
im = im.rotate(90, expand=True)
|
||||
|
||||
def fit_width(image, width):
|
||||
"""Resize an image to desired width"""
|
||||
print('resizing width from', image.width, 'to', end = ' ')
|
||||
wpercent = (display_width/float(image.width))
|
||||
hsize = int((float(image.height)*float(wpercent)))
|
||||
img = image.resize((width, hsize), Image.ANTIALIAS)
|
||||
print(img.width)
|
||||
return img
|
||||
|
||||
def fit_height(image, height):
|
||||
"""Resize an image to desired height"""
|
||||
print('resizing height from', image.height, 'to', end = ' ')
|
||||
hpercent = (height / float(image.height))
|
||||
wsize = int(float(image.width) * float(hpercent))
|
||||
img = image.resize((wsize, height), Image.ANTIALIAS)
|
||||
print(img.height)
|
||||
return img
|
||||
|
||||
if im.width > display_width:
|
||||
im = fit_width(im, display_width)
|
||||
if im.height > display_height:
|
||||
im = fit_height(im, display_height)
|
||||
|
||||
if alignment == 'center':
|
||||
x,y = int((display_width-im.width)/2), int((display_height-im.height)/2)
|
||||
elif alignment == 'center_right':
|
||||
x, y = display_width-im.width, int((display_height-im.height)/2)
|
||||
elif alignment == 'center_left':
|
||||
x, y = 0, int((display_height-im.height)/2)
|
||||
|
||||
elif alignment == 'top_center':
|
||||
x, y = int((display_width-im.width)/2), 0
|
||||
elif alignment == 'top_right':
|
||||
x, y = display_width-im.width, 0
|
||||
elif alignment == 'top_left':
|
||||
x, y = 0, 0
|
||||
|
||||
elif alignment == 'bottom_center':
|
||||
x, y = int((display_width-im.width)/2), display_height-im.height
|
||||
elif alignment == 'bottom_right':
|
||||
x, y = display_width-im.width, display_height-im.height
|
||||
elif alignment == 'bottom_left':
|
||||
x, y = display_width-im.width, display_height-im.height
|
||||
|
||||
if len(im.getbands()) == 4:
|
||||
print('removing transparency')
|
||||
bg = Image.new('RGBA', (im.width, im.height), 'white')
|
||||
im = Image.alpha_composite(bg, im)
|
||||
|
||||
image.paste(im, (x,y))
|
||||
im = image
|
||||
|
||||
if colours == 'bw':
|
||||
"""For black-white images, use monochrome dithering"""
|
||||
black = im.convert('1', dither=True)
|
||||
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 != 'bw':
|
||||
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
|
||||
colour = Image.fromarray(buffer2)
|
||||
"""Create image for only black pixels"""
|
||||
buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255]
|
||||
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
|
||||
colour = Image.fromarray(buffer2)
|
||||
"""Create image for only black pixels"""
|
||||
buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255]
|
||||
black = Image.fromarray(buffer1)
|
||||
|
||||
if render == True:
|
||||
epaper = driver.EPD()
|
||||
print('Initialising E-Paper...', end = '')
|
||||
epaper.init()
|
||||
print('Done')
|
||||
|
||||
print('Sending image data and refreshing display...', end='')
|
||||
if three_colour_support == True:
|
||||
epaper.display(epaper.getbuffer(black), epaper.getbuffer(colour))
|
||||
else:
|
||||
epaper.display(epaper.getbuffer(black))
|
||||
print('Done')
|
||||
|
||||
print('Sending E-Paper to deep sleep...', end = '')
|
||||
epaper.sleep()
|
||||
print('Done')
|
@ -1,95 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
RSS module for Inky-Calendar software.
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import feedparser
|
||||
from random import shuffle
|
||||
from configuration import *
|
||||
|
||||
"""Add a border to increase readability"""
|
||||
border_top = int(bottom_section_height * 0.05)
|
||||
border_left = int(bottom_section_width * 0.02)
|
||||
|
||||
"""Choose font optimised for the weather section"""
|
||||
font = ImageFont.truetype(NotoSans+'.ttf', rss_fontsize)
|
||||
space_between_lines = 1
|
||||
line_height = font.getsize('hg')[1] + space_between_lines
|
||||
line_width = bottom_section_width - (border_left*2)
|
||||
|
||||
"""Find out how many lines can fit at max in the bottom section"""
|
||||
max_lines = (bottom_section_height - (border_top*2)) // (font.getsize('hg')[1]
|
||||
+ space_between_lines)
|
||||
|
||||
"""Calculate the height padding so the lines look centralised"""
|
||||
y_padding = int( (bottom_section_height % line_height) / 2 )
|
||||
|
||||
"""Create a list containing positions of each line"""
|
||||
line_positions = [(border_left, bottom_section_offset +
|
||||
border_top + y_padding + _*line_height ) for _ in range(max_lines)]
|
||||
|
||||
def generate_image():
|
||||
if bottom_section == "inkycal_rss" and rss_feeds != [] and internet_available() == True:
|
||||
try:
|
||||
clear_image('bottom_section')
|
||||
print('RSS module: Connectivity check passed. Generating image...',
|
||||
end = '')
|
||||
|
||||
"""Parse the RSS-feed titles & summaries and save them to a list"""
|
||||
parsed_feeds = []
|
||||
for feeds in rss_feeds:
|
||||
text = feedparser.parse(feeds)
|
||||
for posts in text.entries:
|
||||
parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary))
|
||||
|
||||
"""Shuffle the list, then crop it to the max number of lines"""
|
||||
shuffle(parsed_feeds)
|
||||
del parsed_feeds[max_lines:]
|
||||
|
||||
|
||||
"""Check the lenght of each feed. Wrap the text if it doesn't fit on one line"""
|
||||
flatten = lambda z: [x for y in z for x in y]
|
||||
filtered_feeds, counter = [], 0
|
||||
|
||||
for posts in parsed_feeds:
|
||||
wrapped = text_wrap(posts, font = font, line_width = line_width)
|
||||
counter += len(filtered_feeds) + len(wrapped)
|
||||
if counter < max_lines:
|
||||
filtered_feeds.append(wrapped)
|
||||
filtered_feeds = flatten(filtered_feeds)
|
||||
|
||||
"""Write the correctly formatted text on the display"""
|
||||
for _ in range(len(filtered_feeds)):
|
||||
write_text(line_width, line_height, filtered_feeds[_],
|
||||
line_positions[_], font = font, alignment= 'left')
|
||||
|
||||
del filtered_feeds, parsed_feeds
|
||||
|
||||
rss_image = crop_image(image, 'bottom_section')
|
||||
rss_image.save(image_path+'inkycal_rss.png')
|
||||
|
||||
if three_colour_support == True:
|
||||
rss_image_col = crop_image(image_col, 'bottom_section')
|
||||
rss_image_col.save(image_path+'inkycal_rss_col.png')
|
||||
|
||||
print('Done')
|
||||
|
||||
except Exception as e:
|
||||
"""If something went wrong, print a Error message on the Terminal"""
|
||||
print('Failed!')
|
||||
print('Error in RSS module!')
|
||||
print('Reason: ',e)
|
||||
clear_image('bottom_section')
|
||||
write_text(bottom_section_width, bottom_section_height, str(e),
|
||||
(0, bottom_section_offset), font = font)
|
||||
rss = crop_image(image, 'bottom_section')
|
||||
rss.save(image_path+'inkycal_rss.png')
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
generate_image()
|
||||
|
||||
main()
|
@ -1,362 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Weather module for Inky-Calendar software.
|
||||
|
||||
The lunar phase calculation is from Sean B. Palmer, inamidst.com.
|
||||
Thank You Palmer for the awesome code!
|
||||
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
import pyowm
|
||||
from configuration import *
|
||||
import math, decimal
|
||||
dec = decimal.Decimal
|
||||
|
||||
|
||||
"""Optional parameters"""
|
||||
round_temperature = True
|
||||
round_windspeed = True
|
||||
use_beaufort = True
|
||||
show_wind_direction = False
|
||||
use_wind_direction_icon = False
|
||||
now_str = 'now'
|
||||
|
||||
|
||||
"""Set the optional parameters"""
|
||||
decimal_places_temperature = None if round_temperature == True else 1
|
||||
decimal_places_windspeed = None if round_windspeed == True else 1
|
||||
|
||||
print('Initialising weather...', end=' ')
|
||||
owm = pyowm.OWM(api_key, language=language)
|
||||
print('Done')
|
||||
|
||||
"""Icon-code to unicode dictionary for weather-font"""
|
||||
weathericons = {
|
||||
'01d': '\uf00d', '02d': '\uf002', '03d': '\uf013',
|
||||
'04d': '\uf012', '09d': '\uf01a', '10d': '\uf019',
|
||||
'11d': '\uf01e', '13d': '\uf01b', '50d': '\uf014',
|
||||
'01n': '\uf02e', '02n': '\uf013', '03n': '\uf013',
|
||||
'04n': '\uf013', '09n': '\uf037', '10n': '\uf036',
|
||||
'11n': '\uf03b', '13n': '\uf038', '50n': '\uf023'
|
||||
}
|
||||
|
||||
"""Add a border to increase readability"""
|
||||
border_top = int(top_section_height * 0.05)
|
||||
border_left = int(top_section_width * 0.02)
|
||||
|
||||
"""Calculate size for each weather sub-section"""
|
||||
row_height = (top_section_height-(border_top*2)) // 3
|
||||
coloumn_width = (top_section_width-(border_left*2)) // 7
|
||||
|
||||
"""Calculate paddings"""
|
||||
x_padding = int( (top_section_width % coloumn_width) / 2 )
|
||||
y_padding = int( (top_section_height % row_height) / 2 )
|
||||
|
||||
"""Allocate sizes for weather icons"""
|
||||
icon_small = row_height
|
||||
icon_medium = row_height * 2
|
||||
|
||||
"""Calculate the x-axis position of each coloumn"""
|
||||
coloumn1 = x_padding
|
||||
coloumn2 = coloumn1 + coloumn_width
|
||||
coloumn3 = coloumn2 + coloumn_width
|
||||
coloumn4 = coloumn3 + coloumn_width
|
||||
coloumn5 = coloumn4 + coloumn_width
|
||||
coloumn6 = coloumn5 + coloumn_width
|
||||
coloumn7 = coloumn6 + coloumn_width
|
||||
|
||||
"""Calculate the y-axis position of each row"""
|
||||
row1 = y_padding
|
||||
row2 = row1 + row_height
|
||||
row3 = row2 + row_height
|
||||
|
||||
"""Allocate positions for current weather details"""
|
||||
text_now_pos = (coloumn1, row1)
|
||||
weather_icon_now_pos = (coloumn1, row2)
|
||||
|
||||
temperature_icon_now_pos = (coloumn2, row1)
|
||||
temperature_now_pos = (coloumn2+icon_small, row1)
|
||||
humidity_icon_now_pos = (coloumn2, row2)
|
||||
humidity_now_pos = (coloumn2+icon_small, row2)
|
||||
windspeed_icon_now_pos = (coloumn2, row3)
|
||||
windspeed_now_pos = (coloumn2+icon_small, row3)
|
||||
|
||||
moon_phase_now_pos = (coloumn3, row1)
|
||||
sunrise_icon_now_pos = (coloumn3, row2)
|
||||
sunrise_time_now_pos = (coloumn3+icon_small, row2)
|
||||
sunset_icon_now_pos = (coloumn3, row3)
|
||||
sunset_time_now_pos = (coloumn3+ icon_small, row3)
|
||||
|
||||
"""Allocate positions for weather forecast after 3 hours"""
|
||||
text_fc1_pos = (coloumn4, row1)
|
||||
icon_fc1_pos = (coloumn4, row2)
|
||||
temperature_fc1_pos = (coloumn4, row3)
|
||||
|
||||
"""Allocate positions for weather forecast after 6 hours"""
|
||||
text_fc2_pos = (coloumn5, row1)
|
||||
icon_fc2_pos = (coloumn5, row2)
|
||||
temperature_fc2_pos = (coloumn5, row3)
|
||||
|
||||
"""Allocate positions for weather forecast after 9 hours"""
|
||||
text_fc3_pos = (coloumn6, row1)
|
||||
icon_fc3_pos = (coloumn6, row2)
|
||||
temperature_fc3_pos = (coloumn6, row3)
|
||||
|
||||
"""Allocate positions for weather forecast after 12 hours"""
|
||||
text_fc4_pos = (coloumn7, row1)
|
||||
icon_fc4_pos = (coloumn7, row2)
|
||||
temperature_fc4_pos = (coloumn7, row3)
|
||||
|
||||
"""Windspeed (m/s) to beaufort (index of list) conversion"""
|
||||
windspeed_to_beaufort = [0.02, 1.5, 3.3, 5.4, 7.9, 10.7, 13.8, 17.1, 20.7,
|
||||
24.4, 28.4, 32.6, 100]
|
||||
|
||||
def to_units(kelvin):
|
||||
"""Function to convert tempertures from kelvin to celcius or fahrenheit"""
|
||||
degrees_celsius = round(kelvin - 273.15, ndigits = decimal_places_temperature)
|
||||
fahrenheit = round((kelvin - 273.15) * 9/5 + 32,
|
||||
ndigits = decimal_places_temperature)
|
||||
if units == 'metric':
|
||||
conversion = str(degrees_celsius) + '°C'
|
||||
|
||||
if units == 'imperial':
|
||||
conversion = str(fahrenheit) + 'F'
|
||||
|
||||
return conversion
|
||||
|
||||
def red_temp(negative_temperature):
|
||||
if three_colour_support == True and negative_temperature[0] == '-' and units == 'metric':
|
||||
colour = 'red'
|
||||
else:
|
||||
colour = 'black'
|
||||
return colour
|
||||
|
||||
"""Function to convert time objects to specified format 12/24 hours"""
|
||||
"""Simple means just the hour and if 12 hours, am/pm as well"""
|
||||
def to_hours(datetime_object, simple = False):
|
||||
if hours == '24':
|
||||
if simple == True:
|
||||
converted_time = datetime_object.format('H') + '.00'
|
||||
else:
|
||||
converted_time = datetime_object.format('HH:mm')
|
||||
else:
|
||||
if simple == True:
|
||||
converted_time = datetime_object.format('H a')
|
||||
else:
|
||||
converted_time = datetime_object.format('hh:mm')
|
||||
return str(converted_time)
|
||||
|
||||
"""Choose font optimised for the weather section"""
|
||||
fontsize = 8
|
||||
font = ImageFont.truetype(NotoSans+'Medium.ttf', fontsize)
|
||||
fill_height = 0.8
|
||||
|
||||
while font.getsize('hg')[1] <= (row_height * fill_height):
|
||||
fontsize += 1
|
||||
font = ImageFont.truetype(NotoSans+'.ttf', fontsize)
|
||||
|
||||
def generate_image():
|
||||
"""Connect to Openweathermap API and fetch weather data"""
|
||||
if top_section == "inkycal_weather" and api_key != "" and owm.is_API_online() is True:
|
||||
try:
|
||||
clear_image('top_section')
|
||||
print('Weather module: Connectivity check passed, Generating image...',
|
||||
end = '')
|
||||
current_weather_setup = owm.weather_at_place(location)
|
||||
weather = current_weather_setup.get_weather()
|
||||
|
||||
"""Set-up and get weather forecast data"""
|
||||
forecast = owm.three_hours_forecast(location)
|
||||
|
||||
"""Round the hour to the nearest multiple of 3"""
|
||||
now = arrow.utcnow()
|
||||
if (now.hour % 3) != 0:
|
||||
hour_gap = 3 - (now.hour % 3)
|
||||
else:
|
||||
hour_gap = 3
|
||||
|
||||
"""Prepare timings for forecasts"""
|
||||
fc1 = now.replace(hours = + hour_gap).floor('hour')
|
||||
fc2 = now.replace(hours = + hour_gap + 3).floor('hour')
|
||||
fc3 = now.replace(hours = + hour_gap + 6).floor('hour')
|
||||
fc4 = now.replace(hours = + hour_gap + 9).floor('hour')
|
||||
|
||||
"""Prepare forecast objects for the specified timings"""
|
||||
forecast_fc1 = forecast.get_weather_at(fc1.datetime)
|
||||
forecast_fc2 = forecast.get_weather_at(fc2.datetime)
|
||||
forecast_fc3 = forecast.get_weather_at(fc3.datetime)
|
||||
forecast_fc4 = forecast.get_weather_at(fc4.datetime)
|
||||
|
||||
"""Get the current temperature and forcasts temperatures"""
|
||||
temperature_now = to_units(weather.get_temperature()['temp'])
|
||||
temperature_fc1 = to_units(forecast_fc1.get_temperature()['temp'])
|
||||
temperature_fc2 = to_units(forecast_fc2.get_temperature()['temp'])
|
||||
temperature_fc3 = to_units(forecast_fc3.get_temperature()['temp'])
|
||||
temperature_fc4 = to_units(forecast_fc4.get_temperature()['temp'])
|
||||
|
||||
"""Get current and forecast weather icon names"""
|
||||
weather_icon_now = weather.get_weather_icon_name()
|
||||
weather_icon_fc1 = forecast_fc1.get_weather_icon_name()
|
||||
weather_icon_fc2 = forecast_fc2.get_weather_icon_name()
|
||||
weather_icon_fc3 = forecast_fc3.get_weather_icon_name()
|
||||
weather_icon_fc4 = forecast_fc4.get_weather_icon_name()
|
||||
|
||||
"""Parse current weather details"""
|
||||
sunrise_time_now = arrow.get(weather.get_sunrise_time()).to(get_tz())
|
||||
sunset_time_now = arrow.get(weather.get_sunset_time()).to(get_tz())
|
||||
humidity_now = str(weather.get_humidity())
|
||||
cloudstatus_now = str(weather.get_clouds())
|
||||
weather_description_now = str(weather.get_detailed_status())
|
||||
windspeed_now = weather.get_wind(unit='meters_sec')['speed']
|
||||
wind_degrees = forecast_fc1.get_wind()['deg']
|
||||
wind_direction = ["N","NE","E","SE","S","SW","W","NW"][round(
|
||||
wind_degrees/45) % 8]
|
||||
|
||||
if use_beaufort == True:
|
||||
wind = str([windspeed_to_beaufort.index(_) for _ in
|
||||
windspeed_to_beaufort if windspeed_now < _][0])
|
||||
else:
|
||||
meters_sec = round(windspeed_now, ndigits = windspeed_decimal_places)
|
||||
miles_per_hour = round(windspeed_now * 2,23694,
|
||||
ndigits = windspeed_decimal_places)
|
||||
if units == 'metric':
|
||||
wind = str(meters_sec) + 'm/s'
|
||||
if units == 'imperial':
|
||||
wind = str(miles_per_hour) + 'mph'
|
||||
if show_wind_direction == True:
|
||||
wind += '({0})'.format(wind_direction)
|
||||
|
||||
"""Calculate the moon phase"""
|
||||
def get_moon_phase():
|
||||
diff = now - arrow.get(2001, 1, 1)
|
||||
days = dec(diff.days) + (dec(diff.seconds) / dec(86400))
|
||||
lunations = dec("0.20439731") + (days * dec("0.03386319269"))
|
||||
position = lunations % dec(1)
|
||||
index = math.floor((position * dec(8)) + dec("0.5"))
|
||||
return {0: '\uf095',1: '\uf099',2: '\uf09c',3: '\uf0a0',
|
||||
4: '\uf0a3',5: '\uf0a7',6: '\uf0aa',7: '\uf0ae' }[int(index) & 7]
|
||||
|
||||
moonphase = get_moon_phase()
|
||||
|
||||
"""Add weather details in column 1"""
|
||||
write_text(coloumn_width, row_height, now_str, text_now_pos, font = font)
|
||||
write_text(icon_medium, icon_medium, weathericons[weather_icon_now],
|
||||
weather_icon_now_pos, font = w_font, fill_width = 0.9)
|
||||
|
||||
"""Add weather details in column 2"""
|
||||
write_text(icon_small, icon_small, '\uf053', temperature_icon_now_pos,
|
||||
font = w_font, fill_height = 0.9)
|
||||
write_text(icon_small, icon_small, '\uf07a', humidity_icon_now_pos,
|
||||
font = w_font, fill_height = 0.9)
|
||||
|
||||
if use_wind_direction_icon == False:
|
||||
write_text(icon_small, icon_small, '\uf050', windspeed_icon_now_pos,
|
||||
font = w_font, fill_height = 0.9)
|
||||
else:
|
||||
write_text(icon_small, icon_small, '\uf0b1', windspeed_icon_now_pos,
|
||||
font = w_font, fill_height = 0.9, rotation = -wind_degrees)
|
||||
|
||||
write_text(coloumn_width-icon_small, row_height, temperature_now,
|
||||
temperature_now_pos, font = font, colour= red_temp(temperature_now))
|
||||
|
||||
|
||||
write_text(coloumn_width-icon_small, row_height, humidity_now+'%',
|
||||
humidity_now_pos, font = font)
|
||||
write_text(coloumn_width-icon_small, row_height, wind,
|
||||
windspeed_now_pos, font = font, autofit = True)
|
||||
|
||||
"""Add weather details in column 3"""
|
||||
write_text(coloumn_width, row_height, moonphase , moon_phase_now_pos,
|
||||
font = w_font, fill_height = 0.9)
|
||||
write_text(icon_small, icon_small, '\uf051', sunrise_icon_now_pos,
|
||||
font = w_font, fill_height = 0.9)
|
||||
write_text(icon_small, icon_small, '\uf052', sunset_icon_now_pos,
|
||||
font = w_font, fill_height = 0.9)
|
||||
|
||||
write_text(coloumn_width-icon_small, row_height,
|
||||
to_hours(sunrise_time_now), sunrise_time_now_pos, font = font,
|
||||
fill_width = 0.9)
|
||||
write_text(coloumn_width-icon_small, row_height,
|
||||
to_hours(sunset_time_now), sunset_time_now_pos, font = font,
|
||||
fill_width = 0.9)
|
||||
|
||||
"""Add weather details in column 4 (forecast 1)"""
|
||||
write_text(coloumn_width, row_height, to_hours(fc1, simple=True),
|
||||
text_fc1_pos, font = font)
|
||||
write_text(coloumn_width, row_height, weathericons[weather_icon_fc1],
|
||||
icon_fc1_pos, font = w_font, fill_height = 1.0)
|
||||
write_text(coloumn_width, row_height, temperature_fc1,
|
||||
temperature_fc1_pos, font = font, colour = red_temp(
|
||||
temperature_fc1))
|
||||
|
||||
"""Add weather details in column 5 (forecast 2)"""
|
||||
write_text(coloumn_width, row_height, to_hours(fc2, simple=True),
|
||||
text_fc2_pos, font = font)
|
||||
write_text(coloumn_width, row_height, weathericons[weather_icon_fc2],
|
||||
icon_fc2_pos, font = w_font, fill_height = 1.0)
|
||||
write_text(coloumn_width, row_height, temperature_fc2,
|
||||
temperature_fc2_pos, font = font, colour = red_temp(
|
||||
temperature_fc2))
|
||||
|
||||
"""Add weather details in column 6 (forecast 3)"""
|
||||
write_text(coloumn_width, row_height, to_hours(fc3, simple=True),
|
||||
text_fc3_pos, font = font)
|
||||
write_text(coloumn_width, row_height, weathericons[weather_icon_fc3],
|
||||
icon_fc3_pos, font = w_font, fill_height = 1.0)
|
||||
write_text(coloumn_width, row_height, temperature_fc3,
|
||||
temperature_fc3_pos, font = font, colour = red_temp(
|
||||
temperature_fc3))
|
||||
|
||||
"""Add weather details in coloumn 7 (forecast 4)"""
|
||||
write_text(coloumn_width, row_height, to_hours(fc4, simple=True),
|
||||
text_fc4_pos, font = font)
|
||||
write_text(coloumn_width, row_height, weathericons[weather_icon_fc4],
|
||||
icon_fc4_pos, font = w_font, fill_height = 1.0)
|
||||
write_text(coloumn_width, row_height, temperature_fc4,
|
||||
temperature_fc4_pos, font = font, colour = red_temp(
|
||||
temperature_fc4))
|
||||
|
||||
"""Add vertical lines between forecast sections"""
|
||||
draw = ImageDraw.Draw(image)
|
||||
line_start_y = int(top_section_height*0.1)
|
||||
line_end_y = int(top_section_height*0.9)
|
||||
|
||||
draw.line((coloumn4, line_start_y, coloumn4, line_end_y), fill='black')
|
||||
draw.line((coloumn5, line_start_y, coloumn5, line_end_y), fill='black')
|
||||
draw.line((coloumn6, line_start_y, coloumn6, line_end_y), fill='black')
|
||||
draw.line((coloumn7, line_start_y, coloumn7, line_end_y), fill='black')
|
||||
|
||||
if three_colour_support == True:
|
||||
draw_col.line((0, top_section_height-border_top, top_section_width-
|
||||
border_left, top_section_height-border_top), fill='black', width=3)
|
||||
else:
|
||||
draw.line((0, top_section_height-border_top, top_section_width-
|
||||
border_left, top_section_height-border_top), fill='black', width=3)
|
||||
|
||||
weather_image = crop_image(image, 'top_section')
|
||||
weather_image.save(image_path+'inkycal_weather.png')
|
||||
|
||||
if three_colour_support == True:
|
||||
weather_image_col = crop_image(image_col, 'top_section')
|
||||
weather_image_col.save(image_path+'inkycal_weather_col.png')
|
||||
|
||||
print('Done')
|
||||
|
||||
except Exception as e:
|
||||
"""If something went wrong, print a Error message on the Terminal"""
|
||||
print('Failed!')
|
||||
print('Error in weather module!')
|
||||
print('Reason: ',e)
|
||||
clear_image('top_section')
|
||||
write_text(top_section_width, top_section_height, str(e),
|
||||
(0, 0), font = font)
|
||||
weather_image = crop_image(image, 'top_section')
|
||||
weather_image.save(image_path+'inkycal_weather.png')
|
||||
pass
|
||||
|
||||
def main():
|
||||
generate_image()
|
||||
|
||||
main()
|
@ -1 +0,0 @@
|
||||
v1.7.2
|
@ -1,5 +1,6 @@
|
||||
pyowm==2.10.0
|
||||
Pillow==6.2.0
|
||||
ics==0.4
|
||||
Pillow==7.1.1
|
||||
ics==0.7
|
||||
feedparser==5.2.1
|
||||
pytz==2019.3
|
||||
python-dateutil==2.6.1
|
||||
numpy==1.18.2
|
||||
|
@ -1,306 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Advanced configuration options for Inky-Calendar software.
|
||||
Contains some useful functions for correctly rendering text,
|
||||
calibrating (E-Paper display), checking internet connectivity
|
||||
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageColor
|
||||
import numpy
|
||||
import arrow
|
||||
from urllib.request import urlopen
|
||||
from settings import *
|
||||
from pytz import timezone
|
||||
import os
|
||||
from glob import glob
|
||||
import importlib
|
||||
import subprocess as subp
|
||||
|
||||
"""Set the image background colour and text colour"""
|
||||
background_colour = 'white'
|
||||
text_colour = 'black'
|
||||
|
||||
"""Set some display parameters"""
|
||||
driver = importlib.import_module('drivers.'+model)
|
||||
display_height, display_width = driver.EPD_WIDTH, driver.EPD_HEIGHT
|
||||
|
||||
"""Check if the display supports 3 colours"""
|
||||
if 'colour' in model:
|
||||
three_colour_support = True
|
||||
else:
|
||||
three_colour_support = False
|
||||
|
||||
"""Create 3 sections of the display, based on percentage"""
|
||||
top_section_width = middle_section_width = bottom_section_width = display_width
|
||||
|
||||
if top_section and bottom_section:
|
||||
top_section_height = int(display_height*0.11)
|
||||
bottom_section_height = int(display_height*0.24)
|
||||
|
||||
elif top_section and not bottom_section:
|
||||
top_section_height = int(display_height*0.11)
|
||||
bottom_section_height = 0
|
||||
|
||||
elif bottom_section and not top_section:
|
||||
top_section_height = 0
|
||||
bottom_section_height = int(display_height*0.24)
|
||||
|
||||
elif not top_section and not bottom_section:
|
||||
top_section_height = bottom_section_height = 0
|
||||
|
||||
middle_section_height = int(display_height - top_section_height -
|
||||
bottom_section_height)
|
||||
|
||||
"""Find out the y-axis position of each section"""
|
||||
top_section_offset = 0
|
||||
middle_section_offset = top_section_height
|
||||
bottom_section_offset = display_height - bottom_section_height
|
||||
|
||||
"""Get the relative path of the Inky-Calendar folder"""
|
||||
path = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/")
|
||||
if path != "" and path[-1] != "/":
|
||||
path += "/"
|
||||
while not path.endswith('/Inky-Calendar/'):
|
||||
path = ''.join(list(path)[:-1])
|
||||
|
||||
"""Select path for saving temporary image files"""
|
||||
image_path = path + 'images/'
|
||||
|
||||
"""Fonts handling"""
|
||||
fontpath = path+'fonts/'
|
||||
NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-'
|
||||
NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed'
|
||||
weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf'
|
||||
|
||||
"""Fontsizes"""
|
||||
default_fontsize = 18
|
||||
agenda_fontsize = 14
|
||||
calendar_fontsize = 14
|
||||
rss_fontsize = 14
|
||||
weather_fontsize = 12
|
||||
|
||||
"""Automatically select correct fonts to support set language"""
|
||||
if language in ['ja','zh','zh_tw','ko']:
|
||||
default = ImageFont.truetype(NotoSansCJK+'Regular.otf', default_fontsize)
|
||||
semi = ImageFont.truetype(NotoSansCJK+'Medium.otf', default_fontsize)
|
||||
bold = ImageFont.truetype(NotoSansCJK+'Bold.otf', default_fontsize)
|
||||
else:
|
||||
default = ImageFont.truetype(NotoSans+'.ttf', default_fontsize)
|
||||
semi = ImageFont.truetype(NotoSans+'Medium.ttf', default_fontsize)
|
||||
bold = ImageFont.truetype(NotoSans+'SemiBold.ttf', default_fontsize)
|
||||
|
||||
w_font = ImageFont.truetype(weatherfont, weather_fontsize)
|
||||
|
||||
"""Create a blank image for black pixels and a colour image for coloured pixels"""
|
||||
image = Image.new('RGB', (display_width, display_height), background_colour)
|
||||
image_col = Image.new('RGB', (display_width, display_height), 'white')
|
||||
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw_col = ImageDraw.Draw(image_col)
|
||||
|
||||
|
||||
"""Custom function to add text on an image"""
|
||||
def write_text(space_width, space_height, text, tuple,
|
||||
font=default, alignment='middle', autofit = False, fill_width = 1.0,
|
||||
fill_height = 0.8, colour = text_colour, rotation = None):
|
||||
"""tuple refers to (x,y) position on display"""
|
||||
if autofit == True or fill_width != 1.0 or fill_height != 0.8:
|
||||
size = 8
|
||||
font = ImageFont.truetype(font.path, size)
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
while text_width < int(space_width * fill_width) and text_height < int(space_height * fill_height):
|
||||
size += 1
|
||||
font = ImageFont.truetype(font.path, size)
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
|
||||
while (text_width, text_height) > (space_width, space_height):
|
||||
text=text[0:-1]
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
if alignment is "" or "middle" or None:
|
||||
x = int((space_width / 2) - (text_width / 2))
|
||||
if alignment is 'left':
|
||||
x = 0
|
||||
if font != w_font:
|
||||
y = int((space_height / 2) - (text_height / 1.7))
|
||||
else:
|
||||
y = y = int((space_height / 2) - (text_height / 2))
|
||||
|
||||
space = Image.new('RGBA', (space_width, space_height))
|
||||
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
|
||||
if rotation != None:
|
||||
space.rotate(rotation, expand = True)
|
||||
|
||||
if colour == 'black' or 'white':
|
||||
image.paste(space, tuple, space)
|
||||
else:
|
||||
image_col.paste(space, tuple, space)
|
||||
|
||||
def clear_image(section, colour = background_colour):
|
||||
"""Clear the image"""
|
||||
width, height = eval(section+'_width'), eval(section+'_height')
|
||||
position = (0, eval(section+'_offset'))
|
||||
box = Image.new('RGB', (width, height), colour)
|
||||
image.paste(box, position)
|
||||
|
||||
if three_colour_support == True:
|
||||
image_col.paste(box, position)
|
||||
|
||||
|
||||
def crop_image(input_image, section):
|
||||
"""Crop an input image to the desired section"""
|
||||
x1, y1 = 0, eval(section+'_offset')
|
||||
x2, y2 = eval(section+'_width'), y1 + eval(section+'_height')
|
||||
image = input_image.crop((x1,y1,x2,y2))
|
||||
return image
|
||||
|
||||
def text_wrap(text, font=default, line_width = display_width):
|
||||
"""Split long text into smaller lists"""
|
||||
counter, padding = 0, 40
|
||||
lines = []
|
||||
if font.getsize(text)[0] < line_width:
|
||||
lines.append(text)
|
||||
else:
|
||||
for i in range(1, len(text.split())+1):
|
||||
line = ' '.join(text.split()[counter:i])
|
||||
if not font.getsize(line)[0] < line_width - padding:
|
||||
lines.append(line)
|
||||
line, counter = '', i
|
||||
if i == len(text.split()) and line != '':
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def draw_square(tuple, radius, width, height, colour=text_colour, line_width=1):
|
||||
"""Draws a square with round corners at position (x,y) from tuple"""
|
||||
x, y, diameter = tuple[0], tuple[1], radius*2
|
||||
line_length = width - diameter
|
||||
|
||||
p1, p2 = (x+radius, y), (x+radius+line_length, y)
|
||||
p3, p4 = (x+width, y+radius), (x+width, y+radius+line_length)
|
||||
p5, p6 = (p2[0], y+height), (p1[0], y+height)
|
||||
p7, p8 = (x, p4[1]), (x,p3[1])
|
||||
c1, c2 = (x,y), (x+diameter, y+diameter)
|
||||
c3, c4 = ((x+width)-diameter, y), (x+width, y+diameter)
|
||||
c5, c6 = ((x+width)-diameter, (y+height)-diameter), (x+width, y+height)
|
||||
c7, c8 = (x, (y+height)-diameter), (x+diameter, y+height)
|
||||
|
||||
if three_colour_support == True:
|
||||
draw_col.line( (p1, p2) , fill=colour, width = line_width)
|
||||
draw_col.line( (p3, p4) , fill=colour, width = line_width)
|
||||
draw_col.line( (p5, p6) , fill=colour, width = line_width)
|
||||
draw_col.line( (p7, p8) , fill=colour, width = line_width)
|
||||
draw_col.arc( (c1, c2) , 180, 270, fill=colour, width=line_width)
|
||||
draw_col.arc( (c3, c4) , 270, 360, fill=colour, width=line_width)
|
||||
draw_col.arc( (c5, c6) , 0, 90, fill=colour, width=line_width)
|
||||
draw_col.arc( (c7, c8) , 90, 180, fill=colour, width=line_width)
|
||||
else:
|
||||
draw.line( (p1, p2) , fill=colour, width = line_width)
|
||||
draw.line( (p3, p4) , fill=colour, width = line_width)
|
||||
draw.line( (p5, p6) , fill=colour, width = line_width)
|
||||
draw.line( (p7, p8) , fill=colour, width = line_width)
|
||||
draw.arc( (c1, c2) , 180, 270, fill=colour, width=line_width)
|
||||
draw.arc( (c3, c4) , 270, 360, fill=colour, width=line_width)
|
||||
draw.arc( (c5, c6) , 0, 90, fill=colour, width=line_width)
|
||||
draw.arc( (c7, c8) , 90, 180, fill=colour, width=line_width)
|
||||
|
||||
|
||||
def internet_available():
|
||||
"""check if the internet is available"""
|
||||
try:
|
||||
urlopen('https://google.com',timeout=5)
|
||||
return True
|
||||
except URLError as err:
|
||||
return False
|
||||
|
||||
|
||||
def get_tz():
|
||||
"""Get the system timezone"""
|
||||
with open('/etc/timezone','r') as file:
|
||||
lines = file.readlines()
|
||||
system_tz = lines[0].rstrip()
|
||||
local_tz = timezone(system_tz)
|
||||
return local_tz
|
||||
|
||||
def fix_ical(ical_url):
|
||||
"""Use iCalendars in compatability mode (without alarms)"""
|
||||
ical = str(urlopen(ical_url).read().decode())
|
||||
beginAlarmIndex = 0
|
||||
while beginAlarmIndex >= 0:
|
||||
beginAlarmIndex = ical.find('BEGIN:VALARM')
|
||||
if beginAlarmIndex >= 0:
|
||||
endAlarmIndex = ical.find('END:VALARM')
|
||||
ical = ical[:beginAlarmIndex] + ical[endAlarmIndex+12:]
|
||||
return ical
|
||||
|
||||
def image_cleanup():
|
||||
"""Delete all files in the image folder"""
|
||||
print('Cleanup of previous images...', end = '')
|
||||
for temp_files in glob(image_path+'*'):
|
||||
os.remove(temp_files)
|
||||
print('Done')
|
||||
|
||||
def optimise_colours(image, threshold=220):
|
||||
buffer = numpy.array(image.convert('RGB'))
|
||||
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
||||
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] #grey->black
|
||||
image = Image.fromarray(buffer)
|
||||
return image
|
||||
|
||||
def calibrate_display(no_of_cycles):
|
||||
"""How many times should each colour be calibrated? Default is 3"""
|
||||
epaper = driver.EPD()
|
||||
epaper.init()
|
||||
|
||||
white = Image.new('1', (display_width, display_height), 'white')
|
||||
black = Image.new('1', (display_width, display_height), 'black')
|
||||
|
||||
print('----------Started calibration of E-Paper display----------')
|
||||
if 'colour' in model:
|
||||
for _ in range(no_of_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, no_of_cycles))
|
||||
else:
|
||||
for _ in range(no_of_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, no_of_cycles))
|
||||
|
||||
print('-----------Calibration complete----------')
|
||||
epaper.sleep()
|
||||
|
||||
def check_for_updates():
|
||||
with open(path+'release.txt','r') as file:
|
||||
lines = file.readlines()
|
||||
installed_release = lines[0].rstrip()
|
||||
|
||||
temp = subp.check_output(['curl','-s','https://github.com/aceisace/Inky-Calendar/releases/latest'])
|
||||
latest_release_url = str(temp).split('"')[1]
|
||||
latest_release = latest_release_url.split('/tag/')[1]
|
||||
|
||||
def get_id(version):
|
||||
if not version.startswith('v'):
|
||||
print('incorrect release format!')
|
||||
v = ''.join(version.split('v')[1].split('.'))
|
||||
if len(v) == 2:
|
||||
v += '0'
|
||||
return int(v)
|
||||
|
||||
if get_id(installed_release) < get_id(latest_release):
|
||||
print('New update available!. Please update to the latest version')
|
||||
print('current release:', installed_release, 'new version:', latest_release)
|
||||
else:
|
||||
print('You are using the latest version of the Inky-Calendar software:', end = ' ')
|
||||
print(installed_release)
|
@ -1 +0,0 @@
|
||||
#nothing in here. What did you expect?
|
@ -1,91 +0,0 @@
|
||||
{
|
||||
"language" : "en", // "en", "de", "fr", "jp" etc.
|
||||
"units" : "metric", // "metric", "imperial"
|
||||
"hours" : 24, // 24, 12
|
||||
"model" : "epd_7_in_5_v2_colour", // For supported E-paper models, see below
|
||||
"update_interval" : 60, // 10, 15, 20, 30, 60
|
||||
"calibration_hours" : [0,12,18], // Do not change unlesss you know what you are doing
|
||||
|
||||
//For now three panels can be defined for three unique locations: 'top', 'middle' and 'bottom'
|
||||
"panels" : [
|
||||
{
|
||||
"location" : "top",
|
||||
"type" : "inkycal_weather",
|
||||
"config" : {
|
||||
"api_key" : "", //Your openweathermap API-KEY -> "api-key"
|
||||
"location" : "Stuttgart, DE" //"City name, Country code"
|
||||
}
|
||||
},
|
||||
{
|
||||
"location" : "middle",
|
||||
"type" : "inkycal_calendar", // "inkycal_calendar" and "inkycal_agenda" have the same parameters, but are displayed differently
|
||||
"config" : {
|
||||
"week_starts_on" : "Monday", //"Sunday", "Monday"...
|
||||
"ical_urls" : [
|
||||
"https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics",
|
||||
"https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"location" : "bottom",
|
||||
"type" : "inkycal_rss",
|
||||
"config" : {
|
||||
"rss_feeds" : [
|
||||
"http://feeds.bbci.co.uk/news/world/rss.xml#",
|
||||
"https://github.com/aceisace/Inky-Calendar/releases.atom"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"location" : "middle",
|
||||
"type" : "inkycal_image",
|
||||
"config" : {
|
||||
/*
|
||||
The url or file path to obtain the image from.
|
||||
The following parameters within accolades ({}) will be substituted:
|
||||
- model
|
||||
- width
|
||||
- height
|
||||
|
||||
Samples
|
||||
The inkycal logo:
|
||||
inkycal_image_path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png'
|
||||
|
||||
A dynamic image with a demo-calendar
|
||||
inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/test/{model}/image?width={width}&height={height}'
|
||||
|
||||
Dynamic image with configurable calendars (see https://inkycal.robertsirre.nl/ and parameter inkycal_image_path_body)
|
||||
inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}'
|
||||
|
||||
inkycal_image_path ='/home/pi/Inky-Calendar/images/canvas.png'
|
||||
*/
|
||||
"image_path" : "https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png",
|
||||
|
||||
/*
|
||||
Optional: inkycal_image_path_body
|
||||
Allows obtaining complexer configure images.
|
||||
When `inkycal_image_path` starts with `http` and `inkycal_image_path_body` is specified, the image is obtained using POST instead of GET.
|
||||
NOTE: structure of the body depends on the web-based image service
|
||||
*/
|
||||
|
||||
// inkycal_image_path_body = [
|
||||
// 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics',
|
||||
// 'https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics'
|
||||
// ]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/*
|
||||
Supported E-Paper models"""
|
||||
epd_7_in_5_v2_colour # 7.5" high-res black-white-red/yellow
|
||||
epd_7_in_5_v2 # 7.5" high-res black-white
|
||||
epd_7_in_5_colour # 7.5" black-white-red/yellow
|
||||
epd_7_in_5 # 7.5" black-white
|
||||
epd_5_in_83_colour # 5.83" black-white-red/yellow
|
||||
epd_5_in_83 # 5.83" black-white
|
||||
epd_4_in_2_colour # 4.2" black-white-red/yellow
|
||||
epd_4_in_2 # 4.2" black-white
|
||||
*/
|
@ -1,66 +0,0 @@
|
||||
ical_urls = ["https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics"] #iCal URLs
|
||||
rss_feeds = ["http://feeds.bbci.co.uk/news/world/rss.xml#"] # Use any RSS feed
|
||||
update_interval = "60" # "10" # "15" # "20" # "30" # "60"
|
||||
api_key = "" # Your openweathermap API-KEY -> "api-key"
|
||||
location = "Stuttgart, DE" # "City name, Country code"
|
||||
week_starts_on = "Monday" # 'Sunday' # Monday
|
||||
calibration_hours = [0,12,18] # Do not change unlesss you know what you are doing
|
||||
model = "epd_7_in_5_v2_colour" # Choose the E-Paper model (see below)
|
||||
language = "en" # "en" # "de" # "fr" # "jp" etc.
|
||||
units = "metric" # "metric" # "imperial"
|
||||
hours = "24" # "24" # "12"
|
||||
top_section = "inkycal_weather" # "inkycal_weather"
|
||||
middle_section = "inkycal_calendar" # "inkycal_agenda" #"inkycal_calendar"
|
||||
bottom_section = "inkycal_rss" # "inkycal_rss"
|
||||
|
||||
"""Adding multiple iCalendar URLs or RSS feed URLs"""
|
||||
# Single URL:
|
||||
# ical_urls/rss_feeds = ["url1"]
|
||||
|
||||
# Multiple URLs:
|
||||
# ical_urls/rss_feeds = ["url1", "url2", "url3"]
|
||||
|
||||
# URLs should have this sign (") on both side -> "url1"
|
||||
# If more than one URL is used, separate each one with a comma -> "url1", "url2"
|
||||
|
||||
########################
|
||||
# inkycal_image config:
|
||||
#
|
||||
# inkycal_image_path
|
||||
# The url or file path to obtain the image from.
|
||||
# The following parameters within accolades ({}) will be substituted:
|
||||
# - model
|
||||
# - width
|
||||
# - height
|
||||
#
|
||||
# Samples :
|
||||
# The inkycal logo:
|
||||
# inkycal_image_path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png'
|
||||
#
|
||||
# A dynamic image with a demo-calendar
|
||||
# inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/test/{model}/image?width={width}&height={height}'
|
||||
#
|
||||
# Dynamic image with configurable calendars (see https://inkycal.robertsirre.nl/ and parameter inkycal_image_path_body)
|
||||
# inkycal_image_path = 'https://inkycal.robertsirre.nl/panel/calendar/{model}?width={width}&height={height}'
|
||||
|
||||
inkycal_image_path ='/home/pi/Inky-Calendar/images/canvas.png'
|
||||
|
||||
# Optional: inkycal_image_path_body
|
||||
# Allows obtaining complexer configure images.
|
||||
# When inkycal_image_path starts with `http` and inkycal_image_path_body is specified, the image is obtained using POST instead of GET.
|
||||
# NOTE: structure of the body depends on the web-based image service
|
||||
# inkycal_image_path_body = [
|
||||
# 'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics',
|
||||
# 'https://www.calendarlabs.com/ical-calendar/ics/101/Netherlands_Holidays.ics'
|
||||
# ]
|
||||
########################
|
||||
|
||||
"""Supported E-Paper models"""
|
||||
# epd_7_in_5_v2_colour # 7.5" high-res black-white-red/yellow
|
||||
# epd_7_in_5_v2 # 7.5" high-res black-white
|
||||
# epd_7_in_5_colour # 7.5" black-white-red/yellow
|
||||
# epd_7_in_5 # 7.5" black-white
|
||||
# epd_5_in_83_colour # 5.83" black-white-red/yellow
|
||||
# epd_5_in_83 # 5.83" black-white
|
||||
# epd_4_in_2_colour # 4.2" black-white-red/yellow
|
||||
# epd_4_in_2 # 4.2" black-white
|
Loading…
Reference in New Issue
Block a user