Improved folder structure
With the new folder structure, users can now easily clone the repo with git clone. The need for renaming and shifting folders is now gone.
This commit is contained in:
parent
cfdd691213
commit
9517017365
92
fonts/NotoSans/LICENSE_OFL.txt
Normal file
92
fonts/NotoSans/LICENSE_OFL.txt
Normal file
@ -0,0 +1,92 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
fonts/NotoSans/NotoSans-SemiCondensed.ttf
Normal file
BIN
fonts/NotoSans/NotoSans-SemiCondensed.ttf
Normal file
Binary file not shown.
BIN
fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf
Normal file
BIN
fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf
Normal file
Binary file not shown.
BIN
fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf
Normal file
BIN
fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf
Normal file
Binary file not shown.
11
fonts/NotoSans/README.txt
Normal file
11
fonts/NotoSans/README.txt
Normal file
@ -0,0 +1,11 @@
|
||||
This package is part of the noto project. Visit
|
||||
google.com/get/noto for more information.
|
||||
|
||||
Built on 2017-10-24 from the following noto repository:
|
||||
-----
|
||||
Repo: noto-fonts
|
||||
Tag: v2017-10-24-phase3-second-cleanup
|
||||
Date: 2017-10-24 12:10:34 GMT
|
||||
Commit: 8ef14e6c606a7a0ef3943b9ca01fd49445620d79
|
||||
|
||||
Remove some files that aren't for release.
|
92
fonts/NotoSansCJK/LICENSE_OFL.txt
Normal file
92
fonts/NotoSansCJK/LICENSE_OFL.txt
Normal file
@ -0,0 +1,92 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
fonts/NotoSansCJK/NotoSansCJKsc-Bold.otf
Normal file
BIN
fonts/NotoSansCJK/NotoSansCJKsc-Bold.otf
Normal file
Binary file not shown.
BIN
fonts/NotoSansCJK/NotoSansCJKsc-Medium.otf
Normal file
BIN
fonts/NotoSansCJK/NotoSansCJKsc-Medium.otf
Normal file
Binary file not shown.
BIN
fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf
Normal file
BIN
fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf
Normal file
Binary file not shown.
11
fonts/NotoSansCJK/README
Normal file
11
fonts/NotoSansCJK/README
Normal file
@ -0,0 +1,11 @@
|
||||
This package is part of the noto project. Visit
|
||||
google.com/get/noto for more information.
|
||||
|
||||
Built on 2017-10-24 from the following noto repository:
|
||||
-----
|
||||
Repo: noto-cjk
|
||||
Tag: v2017-06-01-serif-cjk-1-1
|
||||
Date: 2017-09-20 09:49:40 GMT
|
||||
Commit: 32a5844539f2e348ed36b44e990f9b06d7fb89fe
|
||||
|
||||
Update serif CJK to 1.1.
|
BIN
fonts/WeatherFont/weathericons-regular-webfont.ttf
Normal file
BIN
fonts/WeatherFont/weathericons-regular-webfont.ttf
Normal file
Binary file not shown.
1
modules/init.py
Normal file
1
modules/init.py
Normal file
@ -0,0 +1 @@
|
||||
#nothing in here. What did you expect?
|
118
modules/inkycal.py
Normal file
118
modules/inkycal.py
Normal file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Main script of Inky-Calendar software.
|
||||
Copyright by aceisace
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from configuration import *
|
||||
from settings import *
|
||||
import arrow
|
||||
from time import sleep
|
||||
import gc
|
||||
import inkycal_drivers as drivers
|
||||
|
||||
import inkycal_rss as rss
|
||||
import inkycal_weather as weather
|
||||
import inkycal_calendar as calendar
|
||||
import inkycal_agenda as agenda
|
||||
|
||||
|
||||
display = drivers.EPD()
|
||||
skip_calibration = False
|
||||
|
||||
"""Perepare for execution of main programm"""
|
||||
calibration_countdown = 'initial'
|
||||
image_cleanup()
|
||||
|
||||
"""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)
|
||||
|
||||
"""------------------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
|
||||
display.calibrate_display(3)
|
||||
else:
|
||||
if calibration_countdown % (60 // int(update_interval)) == 0:
|
||||
display.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')
|
||||
|
||||
"""----------------Generating and assembling images------"""
|
||||
if top_section == 'Weather':
|
||||
try:
|
||||
weather.main()
|
||||
weather_image = Image.open(image_path + 'weather.png')
|
||||
image.paste(weather_image, (0, 0))
|
||||
except:
|
||||
pass
|
||||
|
||||
if middle_section == 'Calendar':
|
||||
try:
|
||||
calendar.main()
|
||||
calendar_image = Image.open(image_path + 'calendar.png')
|
||||
image.paste(calendar_image, (0, middle_section_offset))
|
||||
except:
|
||||
pass
|
||||
|
||||
if middle_section == 'Agenda':
|
||||
try:
|
||||
agenda.main()
|
||||
agenda_image = Image.open(image_path + 'agenda.png')
|
||||
image.paste(agenda_image, (0, middle_section_offset))
|
||||
except:
|
||||
pass
|
||||
|
||||
if bottom_section == 'RSS':
|
||||
try:
|
||||
rss.main()
|
||||
rss_image = Image.open(image_path + 'rss.png')
|
||||
image.paste(rss_image, (0, bottom_section_offset))
|
||||
except:
|
||||
pass
|
||||
|
||||
image.save(image_path + 'canvas.png')
|
||||
display.reduce_colours(image)
|
||||
|
||||
"""---------Refreshing E-Paper with newly created image-----------"""
|
||||
display.show_image(image)
|
||||
|
||||
"""--------------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))]
|
||||
|
||||
minutes = [i - now.minute for i in update_timings if i >= now.minute]
|
||||
refresh_countdown = minutes[0]*60 + (60 - now.second)
|
||||
|
||||
print('{0} Minutes left until next refresh'.format(minutes[0]))
|
||||
|
||||
del update_timings, minutes, image
|
||||
sleep(refresh_countdown)
|
141
modules/inkycal_agenda.py
Normal file
141
modules/inkycal_agenda.py
Normal file
@ -0,0 +1,141 @@
|
||||
#!/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*
|
||||
from settings import *
|
||||
import arrow
|
||||
|
||||
fontsize = 12
|
||||
show_events = True
|
||||
print_events = False
|
||||
style = 'D MMM YY HH:mm'
|
||||
|
||||
"""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', 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"""
|
||||
if bottom_section:
|
||||
max_lines = int((middle_section_height - border_top*2) // line_height)
|
||||
else:
|
||||
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 main():
|
||||
try:
|
||||
clear_image('middle_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',
|
||||
'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','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']
|
||||
draw.line((date_col_start, line_pos[events][1],
|
||||
line_width,line_pos[events][1]), fill = 'red' if display_type == 'colour' else '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"""
|
||||
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.save(image_path+'agenda.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)
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
194
modules/inkycal_calendar.py
Normal file
194
modules/inkycal_calendar.py
Normal file
@ -0,0 +1,194 @@
|
||||
#!/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 *
|
||||
from settings import *
|
||||
import arrow
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
print_events = False
|
||||
show_events = True
|
||||
max_event_lines = 4
|
||||
style = "DD MMM"
|
||||
event_icon = 'square' # dot #square
|
||||
|
||||
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
|
||||
|
||||
"""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)]
|
||||
|
||||
event_lines = [(border_left,(bottom_section_offset - events_height)+
|
||||
int(events_height/max_event_lines*_)) for _ in
|
||||
range(max_event_lines)]
|
||||
|
||||
def main():
|
||||
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] != 0:
|
||||
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= 'red' if
|
||||
display_type == 'colour' else '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)
|
||||
|
||||
"""Create some reference points for the current month"""
|
||||
days_current_month = calendar.monthrange(now.year, now.month)[1]
|
||||
month_start = now.replace(days =-now.day+1)
|
||||
month_end = now.replace(days=+(days_current_month-now.day))
|
||||
|
||||
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
|
||||
events.begin < month_end and events.begin.month == now.month]
|
||||
|
||||
"""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)
|
||||
|
||||
|
||||
"""Add a small section showing events of today and tomorrow"""
|
||||
event_list = ['{0} at {1} : {2}'.format('today', event.begin.format(
|
||||
'HH:mm' if hours == 24 else 'hh:mm'), event.name)
|
||||
for event in calendar_events if event.begin.day == now.day]
|
||||
|
||||
event_list += ['{0} at {1} : {2}'.format('tomorrow', 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]
|
||||
|
||||
del event_list[4:]
|
||||
|
||||
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)
|
||||
else:
|
||||
write_text(main_area_width, int(events_height/max_event_lines),
|
||||
'No events today or tomorrow', event_lines[0], alignment='left',
|
||||
fill_height = 0.7)
|
||||
|
||||
"""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+'calendar.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)
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
348
modules/inkycal_drivers.py
Normal file
348
modules/inkycal_drivers.py
Normal file
@ -0,0 +1,348 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Drivers file for Inky-Calendar software.
|
||||
Handles E-Paper display related tasks
|
||||
"""
|
||||
|
||||
from PIL import Image
|
||||
import RPi.GPIO as GPIO
|
||||
from settings import display_type
|
||||
import numpy
|
||||
import spidev
|
||||
import RPi.GPIO as GPIO
|
||||
from time import sleep
|
||||
|
||||
RST_PIN = 17
|
||||
DC_PIN = 25
|
||||
CS_PIN = 8
|
||||
BUSY_PIN = 24
|
||||
|
||||
EPD_WIDTH = 640
|
||||
EPD_HEIGHT = 384
|
||||
|
||||
SPI = spidev.SpiDev(0, 0)
|
||||
|
||||
def epd_digital_write(pin, value):
|
||||
GPIO.output(pin, value)
|
||||
|
||||
def epd_digital_read(pin):
|
||||
return GPIO.input(BUSY_PIN)
|
||||
|
||||
def epd_delay_ms(delaytime):
|
||||
sleep(delaytime / 1000.0)
|
||||
|
||||
def spi_transfer(data):
|
||||
SPI.writebytes(data)
|
||||
|
||||
def epd_init():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(RST_PIN, GPIO.OUT)
|
||||
GPIO.setup(DC_PIN, GPIO.OUT)
|
||||
GPIO.setup(CS_PIN, GPIO.OUT)
|
||||
GPIO.setup(BUSY_PIN, GPIO.IN)
|
||||
SPI.max_speed_hz = 4000000
|
||||
SPI.mode = 0b00
|
||||
return 0;
|
||||
|
||||
# EPD7IN5 commands
|
||||
PANEL_SETTING = 0x00
|
||||
POWER_SETTING = 0x01
|
||||
POWER_OFF = 0x02
|
||||
POWER_OFF_SEQUENCE_SETTING = 0x03
|
||||
POWER_ON = 0x04
|
||||
POWER_ON_MEASURE = 0x05
|
||||
BOOSTER_SOFT_START = 0x06
|
||||
DEEP_SLEEP = 0x07
|
||||
DATA_START_TRANSMISSION_1 = 0x10
|
||||
DATA_STOP = 0x11
|
||||
DISPLAY_REFRESH = 0x12
|
||||
IMAGE_PROCESS = 0x13
|
||||
LUT_FOR_VCOM = 0x20
|
||||
LUT_BLUE = 0x21
|
||||
LUT_WHITE = 0x22
|
||||
LUT_GRAY_1 = 0x23
|
||||
LUT_GRAY_2 = 0x24
|
||||
LUT_RED_0 = 0x25
|
||||
LUT_RED_1 = 0x26
|
||||
LUT_RED_2 = 0x27
|
||||
LUT_RED_3 = 0x28
|
||||
LUT_XON = 0x29
|
||||
PLL_CONTROL = 0x30
|
||||
TEMPERATURE_SENSOR_COMMAND = 0x40
|
||||
TEMPERATURE_CALIBRATION = 0x41
|
||||
TEMPERATURE_SENSOR_WRITE = 0x42
|
||||
TEMPERATURE_SENSOR_READ = 0x43
|
||||
VCOM_AND_DATA_INTERVAL_SETTING = 0x50
|
||||
LOW_POWER_DETECTION = 0x51
|
||||
TCON_SETTING = 0x60
|
||||
TCON_RESOLUTION = 0x61
|
||||
SPI_FLASH_CONTROL = 0x65
|
||||
REVISION = 0x70
|
||||
GET_STATUS = 0x71
|
||||
AUTO_MEASUREMENT_VCOM = 0x80
|
||||
READ_VCOM_VALUE = 0x81
|
||||
VCM_DC_SETTING = 0x82
|
||||
|
||||
class EPD:
|
||||
def __init__(self):
|
||||
self.reset_pin = RST_PIN
|
||||
self.dc_pin = DC_PIN
|
||||
self.busy_pin = BUSY_PIN
|
||||
self.width = EPD_WIDTH
|
||||
self.height = EPD_HEIGHT
|
||||
|
||||
def digital_write(self, pin, value):
|
||||
epd_digital_write(pin, value)
|
||||
|
||||
def digital_read(self, pin):
|
||||
return epd_digital_read(pin)
|
||||
|
||||
def delay_ms(self, delaytime):
|
||||
epd_delay_ms(delaytime)
|
||||
|
||||
def send_command(self, command):
|
||||
self.digital_write(self.dc_pin, GPIO.LOW)
|
||||
spi_transfer([command])
|
||||
|
||||
def send_data(self, data):
|
||||
self.digital_write(self.dc_pin, GPIO.HIGH)
|
||||
spi_transfer([data])
|
||||
|
||||
def init(self):
|
||||
if (epd_init() != 0):
|
||||
return -1
|
||||
self.reset()
|
||||
self.send_command(POWER_SETTING)
|
||||
self.send_data(0x37)
|
||||
self.send_data(0x00)
|
||||
self.send_command(PANEL_SETTING)
|
||||
self.send_data(0xCF)
|
||||
self.send_data(0x08)
|
||||
self.send_command(BOOSTER_SOFT_START)
|
||||
self.send_data(0xc7)
|
||||
self.send_data(0xcc)
|
||||
self.send_data(0x28)
|
||||
self.send_command(POWER_ON)
|
||||
self.wait_until_idle()
|
||||
self.send_command(PLL_CONTROL)
|
||||
self.send_data(0x3c)
|
||||
self.send_command(TEMPERATURE_CALIBRATION)
|
||||
self.send_data(0x00)
|
||||
self.send_command(VCOM_AND_DATA_INTERVAL_SETTING)
|
||||
self.send_data(0x77)
|
||||
self.send_command(TCON_SETTING)
|
||||
self.send_data(0x22)
|
||||
self.send_command(TCON_RESOLUTION)
|
||||
self.send_data(0x02) #source 640
|
||||
self.send_data(0x80)
|
||||
self.send_data(0x01) #gate 384
|
||||
self.send_data(0x80)
|
||||
self.send_command(VCM_DC_SETTING)
|
||||
self.send_data(0x1E) #decide by LUT file
|
||||
self.send_command(0xe5) #FLASH MODE
|
||||
self.send_data(0x03)
|
||||
|
||||
def wait_until_idle(self):
|
||||
while(self.digital_read(self.busy_pin) == 0): # 0: busy, 1: idle
|
||||
self.delay_ms(100)
|
||||
|
||||
def reset(self):
|
||||
self.digital_write(self.reset_pin, GPIO.LOW) # module reset
|
||||
self.delay_ms(200)
|
||||
self.digital_write(self.reset_pin, GPIO.HIGH)
|
||||
self.delay_ms(200)
|
||||
|
||||
def calibrate_display(self, no_of_cycles):
|
||||
"""Function for Calibration"""
|
||||
|
||||
if display_type == 'colour':
|
||||
packets = int(self.width / 2 * self.height)
|
||||
if display_type == 'black_and_white':
|
||||
packets = int(self.width / 4 * self.height)
|
||||
|
||||
white, red, black = 0x33, 0x04, 0x00
|
||||
|
||||
self.init()
|
||||
print('----------Started calibration of E-Paper display----------')
|
||||
for _ in range(no_of_cycles):
|
||||
self.send_command(DATA_START_TRANSMISSION_1)
|
||||
print('Calibrating black...')
|
||||
[self.send_data(black) for i in range(packets)]
|
||||
self.send_command(DISPLAY_REFRESH)
|
||||
self.wait_until_idle()
|
||||
|
||||
if display_type == 'colour':
|
||||
print('Calibrating red...')
|
||||
self.send_command(DATA_START_TRANSMISSION_1)
|
||||
[self.send_data(red) for i in range(packets)]
|
||||
self.send_command(DISPLAY_REFRESH)
|
||||
self.wait_until_idle()
|
||||
|
||||
print('Calibrating white...')
|
||||
self.send_command(DATA_START_TRANSMISSION_1)
|
||||
[self.send_data(white) for i in range(packets)]
|
||||
self.send_command(DISPLAY_REFRESH)
|
||||
self.wait_until_idle()
|
||||
|
||||
print('Cycle {0} of {1} complete'.format(_+1, no_of_cycles))
|
||||
|
||||
print('-----------Calibration complete----------')
|
||||
self.sleep()
|
||||
|
||||
def reduce_colours(self, image):
|
||||
buffer = numpy.array(image)
|
||||
r,g,b = buffer[:,:,0], buffer[:,:,1], buffer[:,:,2]
|
||||
|
||||
if display_type == "colour":
|
||||
buffer[numpy.logical_and(r <= 180, r == g)] = [0,0,0] #black
|
||||
buffer[numpy.logical_and(r >= 150, g >= 150)] = [255,255,255] #white
|
||||
buffer[numpy.logical_and(r >= 150, g <= 90)] = [255,0,0] #red
|
||||
|
||||
if display_type == "black_and_white":
|
||||
buffer[numpy.logical_and(r > 245, g > 245)] = [255,255,255] #white
|
||||
buffer[g < 255] = [0,0,0] #black
|
||||
|
||||
image = Image.fromarray(buffer)
|
||||
return image
|
||||
|
||||
def clear(self, colour='white'):
|
||||
if display_type == 'colour':
|
||||
packets = int(self.width / 2 * self.height)
|
||||
if display_type == 'black_and_white':
|
||||
packets = int(self.width / 4 * self.height)
|
||||
|
||||
if colour == 'white': data = 0x33
|
||||
if colour == 'red': data = 0x04
|
||||
if colour == 'black': data = 0x00
|
||||
|
||||
self.init()
|
||||
self.send_command(DATA_START_TRANSMISSION_1)
|
||||
[self.send_data(data) for _ in range(packets)]
|
||||
self.send_command(DISPLAY_REFRESH)
|
||||
print('waiting until E-Paper is not busy')
|
||||
self.delay_ms(100)
|
||||
self.wait_until_idle()
|
||||
print('E-Paper free')
|
||||
self.sleep()
|
||||
|
||||
def get_frame_buffer(self, image):
|
||||
imwidth, imheight = image.size
|
||||
if imwidth == self.height and imheight == self.width:
|
||||
image = image.rotate(270, expand = True)
|
||||
print('Rotated image by 270 degrees...', end= '')
|
||||
elif imwidth != self.width or imheight != self.height:
|
||||
raise ValueError('Image must be same dimensions as display \
|
||||
({0}x{1}).' .format(self.width, self.height))
|
||||
else:
|
||||
print('Image size OK')
|
||||
imwidth, imheight = image.size
|
||||
|
||||
if display_type == 'colour':
|
||||
buf = [0x00] * int(self.width * self.height / 4)
|
||||
image_grayscale = image.convert('L', dither=None)
|
||||
pixels = image_grayscale.load()
|
||||
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
# Set the bits for the column of pixels at the current position.
|
||||
if pixels[x, y] == 0: # black
|
||||
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
|
||||
elif pixels[x, y] == 76: # convert gray to red
|
||||
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
|
||||
buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2)
|
||||
else: # white
|
||||
buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2)
|
||||
|
||||
if display_type == 'black_and_white':
|
||||
buf = [0x00] * int(self.width * self.height / 8)
|
||||
image_monocolor = image.convert('1')
|
||||
|
||||
pixels = image_monocolor.load()
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
# Set the bits for the column of pixels at the current position.
|
||||
if pixels[x, y] != 0:
|
||||
buf[int((x + y * self.width) / 8)] |= 0x80 >> (x % 8)
|
||||
|
||||
return buf
|
||||
|
||||
def display_frame(self, frame_buffer):
|
||||
self.send_command(DATA_START_TRANSMISSION_1)
|
||||
if display_type == 'colour':
|
||||
for i in range(0, int(self.width / 4 * self.height)):
|
||||
temp1 = frame_buffer[i]
|
||||
j = 0
|
||||
while (j < 4):
|
||||
if ((temp1 & 0xC0) == 0xC0):
|
||||
temp2 = 0x03 #white
|
||||
elif ((temp1 & 0xC0) == 0x00):
|
||||
temp2 = 0x00 #black
|
||||
else:
|
||||
temp2 = 0x04 #red
|
||||
temp2 = (temp2 << 4) & 0xFF
|
||||
temp1 = (temp1 << 2) & 0xFF
|
||||
j += 1
|
||||
if((temp1 & 0xC0) == 0xC0):
|
||||
temp2 |= 0x03 #white
|
||||
elif ((temp1 & 0xC0) == 0x00):
|
||||
temp2 |= 0x00 #black
|
||||
else:
|
||||
temp2 |= 0x04 #red
|
||||
temp1 = (temp1 << 2) & 0xFF
|
||||
self.send_data(temp2)
|
||||
j += 1
|
||||
|
||||
if display_type == 'black_and_white':
|
||||
for i in range(0, 30720):
|
||||
temp1 = frame_buffer[i]
|
||||
j = 0
|
||||
while (j < 8):
|
||||
if(temp1 & 0x80):
|
||||
temp2 = 0x03 #white
|
||||
else:
|
||||
temp2 = 0x00 #black
|
||||
temp2 = (temp2 << 4) & 0xFF
|
||||
temp1 = (temp1 << 1) & 0xFF
|
||||
j += 1
|
||||
if(temp1 & 0x80):
|
||||
temp2 |= 0x03 #white
|
||||
else:
|
||||
temp2 |= 0x00 #black
|
||||
temp1 = (temp1 << 1) & 0xFF
|
||||
self.send_data(temp2)
|
||||
j += 1
|
||||
|
||||
self.send_command(DISPLAY_REFRESH)
|
||||
self.delay_ms(100)
|
||||
self.wait_until_idle()
|
||||
|
||||
def show_image(self, image, reduce_colours = True):
|
||||
print('Initialising E-Paper Display...', end='')
|
||||
self.init()
|
||||
sleep(5)
|
||||
print('Done')
|
||||
|
||||
if reduce_colours == True:
|
||||
print('Optimising Image for E-Paper displays...', end = '')
|
||||
image = self.reduce_colours(image)
|
||||
print('Done')
|
||||
else:
|
||||
print('No colour optimisation done on image')
|
||||
|
||||
print('Creating image buffer and sending it to E-Paper display...', end='')
|
||||
data = self.get_frame_buffer(image)
|
||||
print('Done')
|
||||
print('Refreshing display...', end = '')
|
||||
self.display_frame(data)
|
||||
print('Done')
|
||||
print('Sending E-Paper to deep sleep mode...',end='')
|
||||
self.sleep()
|
||||
print('Done')
|
||||
|
||||
def sleep(self):
|
||||
self.send_command(POWER_OFF)
|
||||
self.wait_until_idle()
|
||||
self.send_command(DEEP_SLEEP)
|
||||
self.send_data(0xa5)
|
59
modules/inkycal_icalendar.py
Normal file
59
modules/inkycal_icalendar.py
Normal file
@ -0,0 +1,59 @@
|
||||
#!/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
|
||||
|
||||
print_events = False
|
||||
style = 'DD MMM YY HH:mm'
|
||||
|
||||
|
||||
def fetch_events():
|
||||
"""Set timelines for filtering upcoming events"""
|
||||
now = arrow.now(tz=get_tz())
|
||||
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]
|
||||
|
||||
"""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)
|
||||
|
||||
if not events.all_day:
|
||||
events.begin = events.begin.to(get_tz())
|
||||
events.end = events.end.to(get_tz())
|
||||
|
||||
|
||||
""" 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
|
85
modules/inkycal_rss.py
Normal file
85
modules/inkycal_rss.py
Normal file
@ -0,0 +1,85 @@
|
||||
#!/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 settings import *
|
||||
from configuration import *
|
||||
|
||||
fontsize = 14
|
||||
|
||||
"""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', 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 main():
|
||||
if bottom_section == "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+'rss.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)
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
354
modules/inkycal_weather.py
Normal file
354
modules/inkycal_weather.py
Normal file
@ -0,0 +1,354 @@
|
||||
#!/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 settings import *
|
||||
from configuration import *
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import arrow
|
||||
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
|
||||
|
||||
|
||||
"""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 display_type == 'colour' 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 main():
|
||||
"""Connect to Openweathermap API and fetch weather data"""
|
||||
if top_section == "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.now(tz=get_tz())
|
||||
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)
|
||||
fc2 = now.replace(hours = + hour_gap + 3)
|
||||
fc3 = now.replace(hours = + hour_gap + 6)
|
||||
fc4 = now.replace(hours = + hour_gap + 9)
|
||||
|
||||
"""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', 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')
|
||||
draw.line((0, top_section_height-border_top, top_section_width-
|
||||
border_left, top_section_height-border_top),
|
||||
fill='red' if display_type == 'colour' else 'black' , width=3)
|
||||
|
||||
weather_image = crop_image(image, 'top_section')
|
||||
weather_image.save(image_path+'weather.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!__________')
|
||||
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)
|
||||
weather_image = crop_image(image, 'top_section')
|
||||
weather_image.save(image_path+'weather.png')
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
191
settings/configuration.py
Normal file
191
settings/configuration.py
Normal file
@ -0,0 +1,191 @@
|
||||
#!/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
|
||||
from urllib.request import urlopen
|
||||
from settings import language
|
||||
from pytz import timezone
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
"""Set the image background colour and text colour"""
|
||||
background_colour = 'white'
|
||||
text_colour = 'black'
|
||||
|
||||
"""Set the display height and width (in pixels)"""
|
||||
display_height, display_width = 640, 384
|
||||
|
||||
"""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)
|
||||
|
||||
"""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'
|
||||
|
||||
"""Automatically select correct fonts to support set language"""
|
||||
if language in ['ja','zh','zh_tw','ko']:
|
||||
default = ImageFont.truetype(NotoSansCJK+'Regular.otf', 18)
|
||||
semi = ImageFont.truetype(NotoSansCJK+'Medium.otf', 18)
|
||||
bold = ImageFont.truetype(NotoSansCJK+'Bold.otf', 18)
|
||||
else:
|
||||
default = ImageFont.truetype(NotoSans+'.ttf', 18)
|
||||
semi = ImageFont.truetype(NotoSans+'Medium.ttf', 18)
|
||||
bold = ImageFont.truetype(NotoSans+'SemiBold.ttf', 18)
|
||||
|
||||
w_font = ImageFont.truetype(weatherfont, 10)
|
||||
|
||||
"""Create image with given parameters"""
|
||||
image = Image.new('RGB', (display_width, display_height), background_colour)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
"""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):
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
while (text_width, text_height) > (space_width, space_height):
|
||||
text=text[0:-1]
|
||||
text_width, text_height = font.getsize(text)
|
||||
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)
|
||||
image.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)
|
||||
|
||||
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)
|
||||
|
||||
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')
|
1
settings/init.py
Normal file
1
settings/init.py
Normal file
@ -0,0 +1 @@
|
||||
#nothing in here. What did you expect?
|
28
settings/settings.py
Normal file
28
settings/settings.py
Normal file
@ -0,0 +1,28 @@
|
||||
ical_urls = ["https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics"]
|
||||
rss_feeds = ["http://feeds.bbci.co.uk/news/world/rss.xml#"] # Use any RSS feed
|
||||
|
||||
|
||||
update_interval = "60" # "15" # "30" # "60"
|
||||
api_key = "" # Your openweathermap API-KEY -> "api-key"
|
||||
location = "Stuttgart, DE" # "City name, Country code"
|
||||
week_starts_on = "Monday" # "Monday" # "Sunday"
|
||||
calibration_hours = [0,12,18] # Do not change unless required
|
||||
display_type = "colour" # "colour" # "black_and_white"
|
||||
language = "en" # "en" # "de" # "fr" # "jp" etc.
|
||||
units = "metric" # "metric" # "imperial"
|
||||
hours = "24" # "24" # "12"
|
||||
top_section = "Weather" # "Weather"
|
||||
middle_section = "Calendar" # "Agenda" #"Calendar"
|
||||
bottom_section = "RSS" # "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"
|
||||
|
Loading…
Reference in New Issue
Block a user