diff --git a/fonts/NotoSans/LICENSE_OFL.txt b/fonts/NotoSans/LICENSE_OFL.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/fonts/NotoSans/LICENSE_OFL.txt @@ -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. diff --git a/fonts/NotoSans/NotoSans-SemiCondensed.ttf b/fonts/NotoSans/NotoSans-SemiCondensed.ttf new file mode 100644 index 0000000..358833d Binary files /dev/null and b/fonts/NotoSans/NotoSans-SemiCondensed.ttf differ diff --git a/fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf b/fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf new file mode 100644 index 0000000..4ed47d3 Binary files /dev/null and b/fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf differ diff --git a/fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf b/fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf new file mode 100644 index 0000000..86562e7 Binary files /dev/null and b/fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf differ diff --git a/fonts/NotoSans/README.txt b/fonts/NotoSans/README.txt new file mode 100644 index 0000000..d228764 --- /dev/null +++ b/fonts/NotoSans/README.txt @@ -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. diff --git a/fonts/NotoSansCJK/LICENSE_OFL.txt b/fonts/NotoSansCJK/LICENSE_OFL.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/fonts/NotoSansCJK/LICENSE_OFL.txt @@ -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. diff --git a/fonts/NotoSansCJK/NotoSansCJKsc-Bold.otf b/fonts/NotoSansCJK/NotoSansCJKsc-Bold.otf new file mode 100644 index 0000000..13314c9 Binary files /dev/null and b/fonts/NotoSansCJK/NotoSansCJKsc-Bold.otf differ diff --git a/fonts/NotoSansCJK/NotoSansCJKsc-Medium.otf b/fonts/NotoSansCJK/NotoSansCJKsc-Medium.otf new file mode 100644 index 0000000..d0a58e1 Binary files /dev/null and b/fonts/NotoSansCJK/NotoSansCJKsc-Medium.otf differ diff --git a/fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf b/fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf new file mode 100644 index 0000000..741201b Binary files /dev/null and b/fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf differ diff --git a/fonts/NotoSansCJK/README b/fonts/NotoSansCJK/README new file mode 100644 index 0000000..908d1e0 --- /dev/null +++ b/fonts/NotoSansCJK/README @@ -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. diff --git a/fonts/WeatherFont/weathericons-regular-webfont.ttf b/fonts/WeatherFont/weathericons-regular-webfont.ttf new file mode 100644 index 0000000..948f0a5 Binary files /dev/null and b/fonts/WeatherFont/weathericons-regular-webfont.ttf differ diff --git a/modules/init.py b/modules/init.py new file mode 100644 index 0000000..9b2c532 --- /dev/null +++ b/modules/init.py @@ -0,0 +1 @@ +#nothing in here. What did you expect? \ No newline at end of file diff --git a/modules/inkycal.py b/modules/inkycal.py new file mode 100644 index 0000000..52956cc --- /dev/null +++ b/modules/inkycal.py @@ -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) diff --git a/modules/inkycal_agenda.py b/modules/inkycal_agenda.py new file mode 100644 index 0000000..ccef4b6 --- /dev/null +++ b/modules/inkycal_agenda.py @@ -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() diff --git a/modules/inkycal_calendar.py b/modules/inkycal_calendar.py new file mode 100644 index 0000000..99f7718 --- /dev/null +++ b/modules/inkycal_calendar.py @@ -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() diff --git a/modules/inkycal_drivers.py b/modules/inkycal_drivers.py new file mode 100644 index 0000000..c329c53 --- /dev/null +++ b/modules/inkycal_drivers.py @@ -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) diff --git a/modules/inkycal_icalendar.py b/modules/inkycal_icalendar.py new file mode 100644 index 0000000..a7bff5b --- /dev/null +++ b/modules/inkycal_icalendar.py @@ -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 diff --git a/modules/inkycal_rss.py b/modules/inkycal_rss.py new file mode 100644 index 0000000..5c2e114 --- /dev/null +++ b/modules/inkycal_rss.py @@ -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() diff --git a/modules/inkycal_weather.py b/modules/inkycal_weather.py new file mode 100644 index 0000000..a2b6489 --- /dev/null +++ b/modules/inkycal_weather.py @@ -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() diff --git a/settings/configuration.py b/settings/configuration.py new file mode 100644 index 0000000..c580e41 --- /dev/null +++ b/settings/configuration.py @@ -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') diff --git a/settings/init.py b/settings/init.py new file mode 100644 index 0000000..9b2c532 --- /dev/null +++ b/settings/init.py @@ -0,0 +1 @@ +#nothing in here. What did you expect? \ No newline at end of file diff --git a/settings/settings.py b/settings/settings.py new file mode 100644 index 0000000..3a8b394 --- /dev/null +++ b/settings/settings.py @@ -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" +