commit
2d3ca3727d
67
Changelog.md
67
Changelog.md
@ -4,11 +4,30 @@ The order is from latest to oldest and structured in the following way:
|
||||
* Version name with date of publishing
|
||||
* Sections with either 'added', 'fixed', 'updated' and 'changed'
|
||||
|
||||
## [1.7] Mid December 2019 (date not confirmed yet)
|
||||
## [1.7.1] Mid January 2020
|
||||
|
||||
### Added
|
||||
* Added support for 4.2", 5.83", 7.5" (v2) E-Paper display
|
||||
* Added driver files for above mentioned E-Paper displays
|
||||
|
||||
### Changed
|
||||
* Slight changes in naming of generated images
|
||||
* Slight changes in importing module names (now using dynamic imports)
|
||||
* Changed driver files for all E-Papers with the latest ones from waveshare (v4)
|
||||
* Slightly changed the way modules are executed
|
||||
|
||||
### Removed
|
||||
* Removed option for selecting colour from settings file
|
||||
|
||||
### Fixed
|
||||
* Fixed a problem where the calibration function would only update half the display on the 7.5" black-white E-Paper
|
||||
* Implemented a possible bugfix for 'begin must be before end' error.
|
||||
|
||||
## [1.7] Mid December 2019
|
||||
|
||||
### Added
|
||||
* Added support for sections (top-,middle-,and bottom section)
|
||||
* Added support for weather forecasts.
|
||||
* Added support for weather forecasts.
|
||||
* Added support for moon phase
|
||||
* Added support for events in Calendar module
|
||||
* Added support for coloured negative temperature
|
||||
@ -16,31 +35,37 @@ The order is from latest to oldest and structured in the following way:
|
||||
* Added support for wind direction in weather module
|
||||
* Added support for decimal places in weather module
|
||||
* Added extra customisation options (see configuration file)
|
||||
* Added support for recurring events
|
||||
* Added forecasts in weather module
|
||||
* Added info about moon phase in weather module
|
||||
* Added info about sunrise and sunset time in weather module
|
||||
* Added support for colour-changing temperature (for coloured E-Paper displays, the temperature will red if it drops below 0°Celcius)
|
||||
* Added support for decimal places in weather section (wind speed, temperature)
|
||||
* Added beaufort scale to show windspeed
|
||||
* Added option to show wind direction with an arrow
|
||||
* Added new event and today icon in Calendar module
|
||||
* Added sections showing upcoming events within Calendar module
|
||||
* Added configuration file for additional configuration options
|
||||
* Added new fonts with better readability
|
||||
* Added support to manually change fontsize in each module
|
||||
* Added more design customisation (text colour, background colours etc.)
|
||||
|
||||
### Changed
|
||||
* Refactoring of software. Split software into several smaller modules
|
||||
* Re-arranged weather section layout
|
||||
* Icons (today, events) are generated on demand
|
||||
* Merged calibration files into inkycal_drivers
|
||||
* Changed layout of Agenda module
|
||||
* Changed icons for marking today on Calendar module
|
||||
* Added more options in function 'write_text'
|
||||
* Text does not have any background colour anymore (transparent)
|
||||
* Optimised calibration function for faster calibration, especially for coloured E-Papers
|
||||
* Changed settings file
|
||||
* Changed folder structure (Full software refactoring)
|
||||
* Split main file into smaller modules, each with a specific task
|
||||
* Changed layout of E-Paper (top_section, middle_section, bottom_section)
|
||||
* Changed settings file, installer and web-UI
|
||||
* Black and white E-Papers now use dithering option to map pixels to either black and white
|
||||
|
||||
### Removed
|
||||
* Removed last-updated feature
|
||||
* Removed all icons stored as images
|
||||
* Removed calibration file (calibration.py)
|
||||
|
||||
* Removed non-readable fonts
|
||||
* Removed all icons in form of image files. The new icons are generated with PIL on the spot
|
||||
* Removed option to reduce colours for black and white E-Papers
|
||||
|
||||
### Fixed
|
||||
* Fixed a few bugs related to the ics library
|
||||
* Fine-tuned image pre-processing (mapping pixels to specific colours)
|
||||
* Fixed a problem where RSS feeds would not display more than one post
|
||||
* Fixed a problem where certain weather icons would not be shown
|
||||
|
||||
* Fixed problem with RSS feeds not displaying more than one feed
|
||||
* Fixed image rendering
|
||||
* Fixed problems when setting the weekstart to Sunday
|
||||
|
||||
## [1.6] Mid May 2019
|
||||
|
||||
|
13
Installer.sh
13
Installer.sh
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# E-Paper-Calendar software installer for Raspberry Pi running Debian 10 (a.k.a. Buster) with Desktop
|
||||
# Version: 1.7 (Early Dec 2019)
|
||||
# Version: 1.7.2 (Mid Feb 2020)
|
||||
|
||||
echo -e "\e[1mPlease select an option from below:"
|
||||
echo -e "\e[97mEnter \e[91m[1]\e[97m to update Inky-Calendar software" #Option 1 : UPDATE
|
||||
@ -90,17 +90,6 @@ if [ "$option" = 1 ] || [ "$option" = 2 ]; then # This happens when installing o
|
||||
# Create symlinks of settings and configuration file
|
||||
ln -s /home/"$USER"/Inky-Calendar/settings/settings.py /home/"$USER"/Inky-Calendar/modules/
|
||||
ln -s /home/"$USER"/Inky-Calendar/settings/configuration.py /home/"$USER"/Inky-Calendar/modules/
|
||||
|
||||
# add a short info
|
||||
cat > /home/pi/Inky-Calendar/Info.txt << EOF
|
||||
This document contains a short info of the Inky-Calendar software version
|
||||
|
||||
Version: 1.7
|
||||
Installer version: 1.7 (Mid December 2019)
|
||||
settings file: /home/$USER/Inky-Calendar/settings/settings.py
|
||||
If the time was set correctly, you installed this software on:
|
||||
$(date)
|
||||
EOF
|
||||
echo ""
|
||||
|
||||
echo -e "\e[97mDo you want the software to start automatically at boot?"
|
||||
|
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
v1.7.1
|
||||
v1.7.2
|
||||
|
||||
Main file of Inky-Calendar software. Creates dynamic images for each section,
|
||||
assembles them and sends it to the E-Paper
|
||||
@ -17,17 +17,35 @@ import gc
|
||||
"""Perepare for execution of main programm"""
|
||||
calibration_countdown = 'initial'
|
||||
skip_calibration = False
|
||||
upside_down = False
|
||||
|
||||
image_cleanup()
|
||||
|
||||
top_section_module = importlib.import_module(top_section)
|
||||
middle_section_module = importlib.import_module(middle_section)
|
||||
bottom_section_module = importlib.import_module(bottom_section)
|
||||
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(
|
||||
@ -53,36 +71,62 @@ while True:
|
||||
'displays causes ghosting')
|
||||
|
||||
|
||||
"""----------------Generating and assembling images------"""
|
||||
"""----------------------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))
|
||||
print('Done')
|
||||
|
||||
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))
|
||||
print('Done')
|
||||
|
||||
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))
|
||||
print('Done')
|
||||
|
||||
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 = '')
|
||||
@ -91,12 +135,11 @@ while True:
|
||||
|
||||
if three_colour_support == True:
|
||||
print('Sending image data and refreshing display...', end='')
|
||||
black_im, red_im = split_colours(image)
|
||||
epaper.display(epaper.getbuffer(black_im), epaper.getbuffer(red_im))
|
||||
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.convert('1', dither=True)))
|
||||
epaper.display(epaper.getbuffer(image))
|
||||
print('Done')
|
||||
|
||||
print('Sending E-Paper to deep sleep...', end = '')
|
||||
|
@ -18,7 +18,7 @@ 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_font_size)
|
||||
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))
|
||||
|
||||
@ -33,10 +33,7 @@ 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"""
|
||||
if bottom_section:
|
||||
max_lines = int((middle_section_height - border_top*2) // line_height)
|
||||
else:
|
||||
max_lines = int(middle_section_height+bottom_section_height -
|
||||
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))
|
||||
@ -104,8 +101,14 @@ def generate_image():
|
||||
agenda_events[events]['date_str'], line_pos[events], font = font)
|
||||
|
||||
previous_date = agenda_events[events]['date']
|
||||
draw.line((date_col_start, line_pos[events][1],
|
||||
line_width,line_pos[events][1]), fill = 'red' if three_colour_support == True else 'black')
|
||||
|
||||
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'],
|
||||
@ -123,12 +126,13 @@ def generate_image():
|
||||
(event_col_start, line_pos[events][1]), alignment = 'left', font = font)
|
||||
|
||||
"""Crop the image to show only the middle section"""
|
||||
if bottom_section:
|
||||
agenda_image = crop_image(image, 'middle_section')
|
||||
else:
|
||||
agenda_image = image.crop((0,middle_section_offset,display_width, display_height))
|
||||
|
||||
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:
|
||||
@ -136,8 +140,16 @@ def generate_image():
|
||||
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()
|
||||
|
||||
|
@ -16,7 +16,7 @@ at_in_your_language = 'at'
|
||||
event_icon = 'square' # dot #square
|
||||
style = "DD MMM"
|
||||
|
||||
font = ImageFont.truetype(NotoSans+'.ttf', calendar_font_size)
|
||||
font = ImageFont.truetype(NotoSans+'.ttf', calendar_fontsize)
|
||||
space_between_lines = 0
|
||||
|
||||
if show_events == True:
|
||||
@ -98,7 +98,7 @@ def generate_image():
|
||||
|
||||
"""Add the numbers on the correct positions"""
|
||||
for i in range(len(calendar_flat)):
|
||||
if calendar_flat[i] != 0:
|
||||
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"""
|
||||
@ -110,11 +110,13 @@ def generate_image():
|
||||
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= 'red' if
|
||||
three_colour_support == True else 'black', outline=None)
|
||||
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)
|
||||
image.paste(icon, current_day_pos, icon)
|
||||
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]
|
||||
@ -152,38 +154,47 @@ def generate_image():
|
||||
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)
|
||||
8, square_size , square_size, colour='black')
|
||||
|
||||
|
||||
"""Add a small section showing events of today and tomorrow"""
|
||||
event_list = ['{0} {1} {2} : {3}'.format(today_in_your_language,
|
||||
at_in_your_language, event.begin.format('HH:mm' if hours == 24 else
|
||||
'hh:mm'), event.name) for event in calendar_events if event.begin.day
|
||||
== now.day and now < event.end]
|
||||
|
||||
event_list += ['{0} {1} {2} : {3}'.format(tomorrow_in_your_language,
|
||||
at_in_your_language, event.begin.format('HH:mm' if hours == 24 else
|
||||
'hh:mm'), event.name) for event in calendar_events if event.begin.day
|
||||
== now.replace(days=1).day]
|
||||
|
||||
event_list = []
|
||||
after_two_days = now.replace(days=2).floor('day')
|
||||
|
||||
event_list += ['{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'), event.name) for event in upcoming_events if event.end >
|
||||
after_two_days]
|
||||
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)], alignment='left',
|
||||
fill_height = 0.7)
|
||||
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], alignment='left',
|
||||
fill_height = 0.7)
|
||||
'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'
|
||||
@ -197,6 +208,10 @@ def generate_image():
|
||||
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:
|
||||
@ -204,6 +219,11 @@ def generate_image():
|
||||
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():
|
||||
|
@ -74,6 +74,7 @@ def fetch_events():
|
||||
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)
|
||||
|
@ -2,82 +2,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Experimental image module for Inky-Calendar software
|
||||
Displays an image on the E-Paper. Currently only supports black and white
|
||||
Displays an image on the E-Paper. Work in progress!
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from PIL import Image
|
||||
from configuration import *
|
||||
import os
|
||||
from os import path
|
||||
from PIL import ImageOps
|
||||
import requests
|
||||
import numpy
|
||||
|
||||
import inkycal_drivers as drivers
|
||||
"""----------------------------------------------------------------"""
|
||||
#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?
|
||||
"""----------------------------------------------------------------"""
|
||||
|
||||
display = drivers.EPD()
|
||||
# First determine dimensions
|
||||
if mode == 'horizontal':
|
||||
display_width, display_height == display_height, display_width
|
||||
|
||||
# Where is the image?
|
||||
path = '/home/pi//Desktop/test.JPG'
|
||||
if mode == 'vertical':
|
||||
pass
|
||||
|
||||
class inkycal_image:
|
||||
|
||||
def __init__(self, path):
|
||||
self.image = Image.open(path)
|
||||
self.im_width = self.image.width
|
||||
self.im_height = self.image.height
|
||||
|
||||
def check_mode(self):
|
||||
if self.image.mode != 'RGB' or 'L' or '1':
|
||||
print('Image mode not supported, converting')
|
||||
self.image = self.image.convert('RGB')
|
||||
|
||||
def preview(self):
|
||||
self.image.save(path+'temp.png')
|
||||
os.system("gpicview "+path+'temp.png')
|
||||
os.system('rm '+path+'temp.png')
|
||||
|
||||
|
||||
def check_size(self, alignment = 'middle', padding_colour='white'):
|
||||
if display_height < self.im_height or display_width < self.im_width:
|
||||
print('Image too large for the display, cropping image')
|
||||
if alignment == 'middle' or None:
|
||||
x1 = int((self.im_width - display_width) / 2)
|
||||
y1 = int((self.im_height - display_height) / 2)
|
||||
x2,y2 = x1+display_width, y1+display_height
|
||||
self.image = self.image.crop((x1,y1,x2,y2))
|
||||
|
||||
if alignment != 'middle' or None:
|
||||
print('Sorry, this feature has not been implemented yet')
|
||||
raise NotImplementedError
|
||||
|
||||
elif display_height > self.im_height and display_width > self.im_width:
|
||||
print('Image smaller than display, shifting image to center')
|
||||
x = int( (display_width - self.im_width) /2)
|
||||
y = int( (display_height - self.im_height) /2)
|
||||
canvas = Image.new('RGB', (display_width, display_height), color=padding_colour)
|
||||
canvas.paste(self.image, (x,y))
|
||||
self.image = canvas
|
||||
# .. 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:
|
||||
print('Image file exact. no further action required')
|
||||
# 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
|
||||
|
||||
def auto_flip(self):
|
||||
if self.im_height < self.im_width:
|
||||
print('rotating image')
|
||||
self.image = self.image.rotate(270, expand=True)
|
||||
self.im_width = self.image.width
|
||||
self.im_height = self.image.height
|
||||
|
||||
|
||||
def to_mono(self):
|
||||
self.image = self.image.convert('1', dither=True)
|
||||
"""Turn image upside-down if specified"""
|
||||
if upside_down == True:
|
||||
im.rotate(180, expand = True)
|
||||
|
||||
def prepare_image(self, alignment='middle'):
|
||||
self.check_mode()
|
||||
self.auto_flip()
|
||||
self.check_size(alignment = alignment)
|
||||
self.to_mono()
|
||||
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)
|
||||
|
||||
return self.image
|
||||
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
|
||||
|
||||
#single line command:
|
||||
display.show_image(inkycal_image(path).prepare_image(), reduce_colours=False)
|
||||
|
||||
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')
|
||||
|
@ -14,7 +14,7 @@ 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_font_size)
|
||||
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)
|
||||
@ -69,6 +69,11 @@ def generate_image():
|
||||
|
||||
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:
|
||||
@ -76,8 +81,14 @@ def generate_image():
|
||||
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()
|
||||
|
||||
|
@ -120,7 +120,7 @@ def to_units(kelvin):
|
||||
ndigits = decimal_places_temperature)
|
||||
if units == 'metric':
|
||||
conversion = str(degrees_celsius) + '°C'
|
||||
|
||||
|
||||
if units == 'imperial':
|
||||
conversion = str(fahrenheit) + 'F'
|
||||
|
||||
@ -171,7 +171,7 @@ def generate_image():
|
||||
forecast = owm.three_hours_forecast(location)
|
||||
|
||||
"""Round the hour to the nearest multiple of 3"""
|
||||
now = arrow.now(tz=get_tz())
|
||||
now = arrow.utcnow()
|
||||
if (now.hour % 3) != 0:
|
||||
hour_gap = 3 - (now.hour % 3)
|
||||
else:
|
||||
@ -258,9 +258,10 @@ def generate_image():
|
||||
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, 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,
|
||||
@ -326,24 +327,31 @@ def generate_image():
|
||||
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')
|
||||
draw.line((0, top_section_height-border_top, top_section_width-
|
||||
border_left, top_section_height-border_top),
|
||||
fill='red' if three_colour_support == 'True' else 'black' , width=3)
|
||||
|
||||
weather_image = crop_image(image, 'top_section')
|
||||
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 no response was received from the openweathermap
|
||||
api server, add the cloud with question mark"""
|
||||
print('__________OWM-ERROR!__________')
|
||||
"""If something went wrong, print a Error message on the Terminal"""
|
||||
print('Failed!')
|
||||
print('Error in weather module!')
|
||||
print('Reason: ',e)
|
||||
write_text(icon_medium, icon_medium, '\uf07b', weather_icon_now_pos,
|
||||
font = w_font, fill_height = 1.0)
|
||||
message = 'No internet connectivity or API timeout'
|
||||
write_text(coloumn_width*6, row_height, message, humidity_icon_now_pos,
|
||||
font = font)
|
||||
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
|
||||
|
1
release.txt
Normal file
1
release.txt
Normal file
@ -0,0 +1 @@
|
||||
v1.7.2
|
@ -16,6 +16,7 @@ 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'
|
||||
@ -34,10 +35,23 @@ else:
|
||||
"""Create 3 sections of the display, based on percentage"""
|
||||
top_section_width = middle_section_width = bottom_section_width = display_width
|
||||
|
||||
top_section_height = int(display_height*0.11)
|
||||
middle_section_height = int(display_height*0.65)
|
||||
bottom_section_height = int(display_height - middle_section_height -
|
||||
top_section_height)
|
||||
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
|
||||
@ -60,48 +74,52 @@ NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-'
|
||||
NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed'
|
||||
weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf'
|
||||
|
||||
"""Fonts sizes"""
|
||||
default_font_size = 18
|
||||
agenda_font_size = 14
|
||||
calendar_font_size = 16
|
||||
rss_font_size = 14
|
||||
weather_font_size = 12
|
||||
"""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_font_size)
|
||||
semi = ImageFont.truetype(NotoSansCJK+'Medium.otf', default_font_size)
|
||||
bold = ImageFont.truetype(NotoSansCJK+'Bold.otf', default_font_size)
|
||||
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_font_size)
|
||||
semi = ImageFont.truetype(NotoSans+'Medium.ttf', default_font_size)
|
||||
bold = ImageFont.truetype(NotoSans+'SemiBold.ttf', default_font_size)
|
||||
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_font_size)
|
||||
w_font = ImageFont.truetype(weatherfont, weather_fontsize)
|
||||
|
||||
"""Create image with given parameters"""
|
||||
"""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)
|
||||
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)
|
||||
text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1]
|
||||
|
||||
text_width, text_height = font.getsize(text)
|
||||
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)
|
||||
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':
|
||||
@ -115,7 +133,11 @@ def write_text(space_width, space_height, text, tuple,
|
||||
ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font)
|
||||
if rotation != None:
|
||||
space.rotate(rotation, expand = True)
|
||||
image.paste(space, tuple, space)
|
||||
|
||||
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"""
|
||||
@ -124,6 +146,10 @@ def clear_image(section, colour = background_colour):
|
||||
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')
|
||||
@ -152,8 +178,8 @@ 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)
|
||||
|
||||
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])
|
||||
@ -161,15 +187,26 @@ def draw_square(tuple, radius, width, height, colour=text_colour, line_width=1):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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"""
|
||||
@ -206,23 +243,12 @@ def image_cleanup():
|
||||
os.remove(temp_files)
|
||||
print('Done')
|
||||
|
||||
def split_colours(image):
|
||||
if three_colour_support == True:
|
||||
"""Split image into two, one for red pixels, the other for black pixels"""
|
||||
buffer = numpy.array(image.convert('RGB'))
|
||||
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
||||
buffer_red, buffer_black = numpy.array(image), numpy.array(image)
|
||||
|
||||
buffer_red[numpy.logical_and(red >= 200, green <= 90)] = [0,0,0] #red->black
|
||||
red1 = buffer_red[:,:,0]
|
||||
buffer_red[red1 != 0] = [255,255,255] #white
|
||||
red_im = Image.fromarray(buffer_red).convert('1',dither=True).rotate(270,expand=True)
|
||||
|
||||
buffer_black[numpy.logical_and(red <= 180, red == green)] = [0,0,0] #black
|
||||
red2 = buffer_black[:,:,0]
|
||||
buffer_black[red2 != 0] = [255,255,255] # white
|
||||
black_im = Image.fromarray(buffer_black).convert('1', dither=True).rotate(270,expand=True)
|
||||
return black_im, red_im
|
||||
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"""
|
||||
@ -235,20 +261,46 @@ def calibrate_display(no_of_cycles):
|
||||
print('----------Started calibration of E-Paper display----------')
|
||||
if 'colour' in model:
|
||||
for _ in range(no_of_cycles):
|
||||
print('Calibrating black...')
|
||||
print('Calibrating...', end= ' ')
|
||||
print('black...', end= ' ')
|
||||
epaper.display(epaper.getbuffer(black), epaper.getbuffer(white))
|
||||
print('Calibrating red/yellow...')
|
||||
print('colour...', end = ' ')
|
||||
epaper.display(epaper.getbuffer(white), epaper.getbuffer(black))
|
||||
print('Calibrating white...')
|
||||
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 black...')
|
||||
print('Calibrating...', end= ' ')
|
||||
print('black...', end = ' ')
|
||||
epaper.display(epaper.getbuffer(black))
|
||||
print('Calibrating white...')
|
||||
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)
|
||||
|
@ -35,7 +35,7 @@ body{
|
||||
<input id="ical_urls" type="text" placeholder="'https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics'">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field">
|
||||
<label>RSS-Feed URL/s in the following format: 'URL 1', 'URL 2', 'URL 3'</label>
|
||||
<input id="rss_urls" type="text" placeholder="'http://feeds.bbci.co.uk/news/world/rss.xml#'">
|
||||
</div>
|
||||
@ -78,7 +78,6 @@ body{
|
||||
<input id="api_key" type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field">
|
||||
<label>Location (for weather data)</label>
|
||||
<details class="ts accordion">
|
||||
@ -273,7 +272,7 @@ body{
|
||||
|
||||
<div class="field">
|
||||
<label>What should be displayed in the middle (main) section?</label>
|
||||
<div class="ts checkboxes">
|
||||
<div class="ts checkboxes" id="cb_middle_section">
|
||||
<div class="ts radio checkbox">
|
||||
<input id="Calendar" type="radio" name="ms" checked>
|
||||
<label for="Calendar">A monthly Calendar</label>
|
||||
@ -282,11 +281,52 @@ body{
|
||||
<input id="Agenda" type="radio" name="ms">
|
||||
<label for="Agenda">Agenda of upcoming events</label>
|
||||
</div>
|
||||
<div class="ts radio checkbox">
|
||||
<input id="Image" type="radio" name="ms">
|
||||
<label for="Image">An image</label>
|
||||
</div>
|
||||
<div class="ts radio checkbox">
|
||||
<input id="middle_blank" type="radio" name="ms">
|
||||
<label for="middle_blank">Nothing</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="field" id="Image_Config" style="display:none;">
|
||||
<div class="field">
|
||||
<label>What is the URl or path of the image?</label>
|
||||
<details class="ts accordion">
|
||||
<summary>
|
||||
<i class="dropdown icon"></i> Info
|
||||
</summary>
|
||||
<div class="content">
|
||||
The following parameters will be substituted:
|
||||
<ul>
|
||||
<li><code>{model}</code> - substituted by the E-Paper model name.</li>
|
||||
<li><code>{width}</code> - substituted by the panel width.</li>
|
||||
<li><code>{height}</code> - substituted by the panel width.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
<input id="image_path" type="text" placeholder="https://github.com/aceisace/Inky-Calendar/blob/master/Gallery/Inky-Calendar-logo.png?raw=true"/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Do you want to send extra data while obtaining the image?</label>
|
||||
<details class="ts accordion">
|
||||
<summary>
|
||||
<i class="dropdown icon"></i> Info
|
||||
</summary>
|
||||
<div class="content">
|
||||
<p>Optional data. When specified, this data is sent as Json to the image url using POST.
|
||||
<br/>This is useful for some dynamically generated images.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
<textarea id="image_path_body" type="text" rows="4" placeholder='[
|
||||
"https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics"
|
||||
]'></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
@ -305,13 +345,14 @@ body{
|
||||
|
||||
<div class="content">
|
||||
<p>Instructions<br>
|
||||
Insert your peesonal details and preferences and click on 'Generate'. Copy the downloaded file to the Raspberry Pi and place it in: '/home/pi/Inky-Calendar/settings/' (inside the settings folder within the Inky-Calendar folder. Lastly, reboot the Raspberry Pi to apply the changes. You can also manually run the software with:
|
||||
Insert your personal details and preferences and click on 'Generate'. Copy the downloaded file to the Raspberry Pi and place it in: '/home/pi/Inky-Calendar/settings/' (inside the settings folder within the Inky-Calendar folder. Lastly, reboot the Raspberry Pi to apply the changes. You can also manually run the software with:
|
||||
python3 /home/pi/Inky-Calendar/modules/inkycal.py.</p>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<br>
|
||||
<button class="ts primary button" onClick="generate();">Generate</button>
|
||||
<button class="ts primary button" onClick="generate(false);">Generate</button>
|
||||
<button class="ts secondary button" onClick="generate(true);">Generate (as JSON, experimental)</button>
|
||||
<br><br>
|
||||
<kbd>Developed by Toby Chui for Inky-Calendar Project, modified by aceisace. Licensed under MIT</kbd>
|
||||
<details class="ts accordion">
|
||||
@ -333,9 +374,32 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
<br>
|
||||
|
||||
<script>
|
||||
var template = 'ical_urls = [{ical_urls}]\nrss_feeds = [{rss_urls}]\nupdate_interval = "{update_interval}"\napi_key = "{api_key}"\nlocation = "{location}"\nweek_starts_on = "{week_starts_on}"\ncalibration_hours = [{calibration_hours}]\nmodel = "{model}"\nlanguage = "{language}"\nunits = "{units}"\nhours = "{hours}"\ntop_section = "{top_section}"\nmiddle_section = "{middle_section}"\nbottom_section = "{bottom_section}"';
|
||||
var template = `ical_urls = [{ical_urls}]
|
||||
rss_feeds = [{rss_urls}]
|
||||
update_interval = "{update_interval}"
|
||||
api_key = "{api_key}"
|
||||
location = "{location}"
|
||||
week_starts_on = "{week_starts_on}"
|
||||
calibration_hours = [{calibration_hours}]
|
||||
model = "{model}"
|
||||
language = "{language}"
|
||||
units = "{units}"
|
||||
hours = "{hours}"
|
||||
top_section = "{top_section}"
|
||||
middle_section = "{middle_section}"
|
||||
bottom_section = "{bottom_section}"
|
||||
inkycal_image_path = "{image_path}"
|
||||
inkycal_image_path_body = "{image_path_body}"`;
|
||||
|
||||
function generate(){
|
||||
$('#cb_middle_section').change(function(){
|
||||
if($('#Image').prop("checked")) {
|
||||
$('#Image_Config').show();
|
||||
} else {
|
||||
$('#Image_Config').hide();
|
||||
}
|
||||
});
|
||||
|
||||
function generate(json){
|
||||
var ical_urls = $("#ical_urls").val().trim();
|
||||
if (ical_urls == ""){
|
||||
ical_urls = $("#ical_urls").attr("placeholder");
|
||||
@ -475,6 +539,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
if ($('#Agenda').is(':checked')){
|
||||
middle_section = "inkycal_agenda";
|
||||
}
|
||||
if ($('#Image').is(':checked')){
|
||||
middle_section = "inkycal_image";
|
||||
}
|
||||
if ($('#middle_blank').is(':checked')){
|
||||
middle_section = "";
|
||||
}
|
||||
@ -484,9 +551,18 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
bottom_section = "";
|
||||
}
|
||||
|
||||
var image_path = $("#image_path").val().trim();
|
||||
if (image_path == ""){
|
||||
image_path = $("#image_path").attr("placeholder");
|
||||
}
|
||||
|
||||
var image_path_body = $("#image_path").val().trim();
|
||||
|
||||
//console.log(ical_urls, rss_urls, update_interval, api_key, location, week_starts_on, calibration_hours, model, language, units, hours, top_section, middle_section, bottom_section);
|
||||
createPythonSetting(ical_urls, rss_urls, update_interval, api_key, location, week_starts_on, calibration_hours, model, language, units, hours, top_section, middle_section, bottom_section);
|
||||
if(json)
|
||||
downloadSettingsAsJson(ical_urls, rss_urls, update_interval, api_key, location, week_starts_on, calibration_hours, model, language, units, hours, top_section, middle_section, bottom_section, image_path, image_path_body)
|
||||
else
|
||||
createPythonSetting(ical_urls, rss_urls, update_interval, api_key, location, week_starts_on, calibration_hours, model, language, units, hours, top_section, middle_section, bottom_section, image_path, image_path_body);
|
||||
}
|
||||
|
||||
function rk(content,key,value){
|
||||
@ -494,7 +570,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
return content.split("{" + key + "}").join(value);
|
||||
}
|
||||
|
||||
function createPythonSetting(a,b,c,d,e,f,g,h,i,j,k,l,m,n){
|
||||
function createPythonSetting(a,b,c,d,e,f,g,h,i,j,k,l,m,n, image_path, image_path_body){
|
||||
var box = template;
|
||||
box = rk(box,"ical_urls",a);
|
||||
box = rk(box,"rss_urls",b);
|
||||
@ -510,7 +586,8 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
box = rk(box,"top_section",l);
|
||||
box = rk(box,"middle_section",m);
|
||||
box = rk(box,"bottom_section",n);
|
||||
|
||||
box = rk(box,"image_path",image_path);
|
||||
box = rk(box,"image_path_body",image_path_body);
|
||||
|
||||
var config = new Blob([box], {type : "text/plain"});
|
||||
var link = document.createElement('link');
|
||||
@ -523,6 +600,112 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
function TrimSingleQuotes(text){
|
||||
return text.replace(/^'+/g,"").replace(/'+$/g,"")
|
||||
}
|
||||
|
||||
function downloadSettingsAsJson(
|
||||
ical_urls,
|
||||
rss_urls,
|
||||
update_interval,
|
||||
api_key,
|
||||
location,
|
||||
week_starts_on,
|
||||
calibration_hours,
|
||||
model,
|
||||
language,
|
||||
units,
|
||||
hours,
|
||||
top_section,
|
||||
middle_section,
|
||||
bottom_section,
|
||||
image_path,
|
||||
image_path_body
|
||||
) {
|
||||
var result = {
|
||||
"language" : language, // "en", "de", "fr", "jp" etc.
|
||||
"units" : units, // "metric", "imperial"
|
||||
"hours" : Number(hours), // 24, 12
|
||||
"model" : model,
|
||||
"update_interval" : Number(update_interval), // 10, 15, 20, 30, 60
|
||||
"calibration_hours" : calibration_hours.split(",").map(function(x){ return Number(x);}), // Do not change unless you know what you are doing
|
||||
"panels" : []
|
||||
};
|
||||
|
||||
switch(top_section){
|
||||
case "inkycal_weather":
|
||||
result.panels.push(
|
||||
{
|
||||
"location" : "top",
|
||||
"type" : "inkycal_weather",
|
||||
"config" : {
|
||||
"api_key" : api_key, //Your openweathermap API-KEY -> "api-key"
|
||||
"location" : location //"City name, Country code"
|
||||
}
|
||||
}
|
||||
)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch(middle_section){
|
||||
case "inkycal_agenda":
|
||||
case "inkycal_calendar":
|
||||
result.panels.push(
|
||||
{
|
||||
"location" : "middle",
|
||||
"type" : middle_section,
|
||||
"config" : {
|
||||
"week_starts_on" : week_starts_on, //"Sunday", "Monday"...
|
||||
"ical_urls" : ical_urls.split().map(function(x){ return TrimSingleQuotes(x);})
|
||||
}
|
||||
}
|
||||
)
|
||||
break;
|
||||
case "inkycal_image":
|
||||
result.panels.push(
|
||||
{
|
||||
"location" : "middle",
|
||||
"type" : middle_section,
|
||||
"config" : {
|
||||
"image_path" : TrimSingleQuotes(image_path),
|
||||
"image_path_body" : image_path_body
|
||||
}
|
||||
}
|
||||
)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch(bottom_section){
|
||||
case "inkycal_rss":
|
||||
result.panels.push(
|
||||
{
|
||||
"location" : "bottom",
|
||||
"type" : bottom_section,
|
||||
"config" : {
|
||||
"rss_urls" : rss_urls.split().map(function(x){ return TrimSingleQuotes(x);})
|
||||
}
|
||||
}
|
||||
)
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
var config = new Blob([JSON.stringify(result, null, "\t")], {type : "text/json"});
|
||||
var link = document.createElement('link');
|
||||
link.href = window.URL.createObjectURL(config);
|
||||
var a = document.createElement('A');
|
||||
a.href = link.href;
|
||||
a.download = link.href.substr(link.href.lastIndexOf('/') + 1);
|
||||
document.body.appendChild(a);
|
||||
$(a).attr('download','settings.jsonc');
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
91
settings/settings.jsonc
Normal file
91
settings/settings.jsonc
Normal file
@ -0,0 +1,91 @@
|
||||
{
|
||||
"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
|
||||
*/
|
@ -23,6 +23,38 @@ bottom_section = "inkycal_rss" # "inkycal_rss"
|
||||
# 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
|
||||
|
Loading…
Reference in New Issue
Block a user