diff --git a/Inky-Calendar/fonts/NotoSans/LICENSE_OFL.txt b/Inky-Calendar/fonts/NotoSans/LICENSE_OFL.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/Inky-Calendar/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/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensed.ttf b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensed.ttf new file mode 100644 index 0000000..358833d Binary files /dev/null and b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensed.ttf differ diff --git a/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedExtraLight.ttf b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedExtraLight.ttf new file mode 100644 index 0000000..05db9fd Binary files /dev/null and b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedExtraLight.ttf differ diff --git a/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedLight.ttf b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedLight.ttf new file mode 100644 index 0000000..e175b03 Binary files /dev/null and b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedLight.ttf differ diff --git a/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf new file mode 100644 index 0000000..4ed47d3 Binary files /dev/null and b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedMedium.ttf differ diff --git a/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf new file mode 100644 index 0000000..86562e7 Binary files /dev/null and b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedSemiBold.ttf differ diff --git a/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedThin.ttf b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedThin.ttf new file mode 100644 index 0000000..74cb50c Binary files /dev/null and b/Inky-Calendar/fonts/NotoSans/NotoSans-SemiCondensedThin.ttf differ diff --git a/Inky-Calendar/fonts/NotoSans/README.txt b/Inky-Calendar/fonts/NotoSans/README.txt new file mode 100644 index 0000000..d228764 --- /dev/null +++ b/Inky-Calendar/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/Inky-Calendar/fonts/NotoSansCJK/LICENSE_OFL.txt b/Inky-Calendar/fonts/NotoSansCJK/LICENSE_OFL.txt new file mode 100644 index 0000000..d952d62 --- /dev/null +++ b/Inky-Calendar/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/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-DemiLight.otf b/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-DemiLight.otf new file mode 100644 index 0000000..34a739b Binary files /dev/null and b/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-DemiLight.otf differ diff --git a/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-Light.otf b/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-Light.otf new file mode 100644 index 0000000..cb814c3 Binary files /dev/null and b/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-Light.otf differ diff --git a/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf b/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf new file mode 100644 index 0000000..741201b Binary files /dev/null and b/Inky-Calendar/fonts/NotoSansCJK/NotoSansCJKsc-Regular.otf differ diff --git a/Inky-Calendar/fonts/NotoSansCJK/README b/Inky-Calendar/fonts/NotoSansCJK/README new file mode 100644 index 0000000..908d1e0 --- /dev/null +++ b/Inky-Calendar/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/Inky-Calendar/fonts/WeatherFont/weathericons-regular-webfont.ttf b/Inky-Calendar/fonts/WeatherFont/weathericons-regular-webfont.ttf new file mode 100644 index 0000000..948f0a5 Binary files /dev/null and b/Inky-Calendar/fonts/WeatherFont/weathericons-regular-webfont.ttf differ diff --git a/Inky-Calendar/modules/cal.png b/Inky-Calendar/modules/cal.png new file mode 100644 index 0000000..29925a2 Binary files /dev/null and b/Inky-Calendar/modules/cal.png differ diff --git a/Inky-Calendar/modules/calibration.py b/Inky-Calendar/modules/calibration.py new file mode 100644 index 0000000..4a3ba52 --- /dev/null +++ b/Inky-Calendar/modules/calibration.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Calibration module for the Black-White and Black-White-Red E-Paper display +Calibration refers to flushing all pixels in a single colour to prevent +ghosting. +""" + +from __future__ import print_function +import time +from settings import display_colours +from image_data import black, white, red + +def calibration(): + """Function for Calibration""" + import e_paper_drivers + epd = e_paper_drivers.EPD() + print('_________Calibration for E-Paper started_________'+'\n') + + for i in range(2): + epd.init() + print('Calibrating black...') + epd.display_frame(epd.get_frame_buffer(black)) + if display_colours == "bwr": + print('calibrating red...') + epd.display_frame(epd.get_frame_buffer(red)) + print('Calibrating white...') + epd.display_frame(epd.get_frame_buffer(white)) + epd.sleep() + print('Cycle', str(i+1)+'/2', 'complete'+'\n') + print('Calibration complete') + +def main(): + """Added timer""" + start = time.time() + calibration() + end = time.time() + print('Calibration complete in', int(end - start), 'seconds') + +if __name__ == '__main__': + main() diff --git a/Inky-Calendar/modules/configuration.py b/Inky-Calendar/modules/configuration.py new file mode 100644 index 0000000..bb21264 --- /dev/null +++ b/Inky-Calendar/modules/configuration.py @@ -0,0 +1,171 @@ +""" +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 +from urllib.request import urlopen +from settings import language +from pytz import timezone +import numpy as np +import os + +"""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.10) +middle_section_height = int(display_height*0.65) +bottom_section_height = int(display_height - middle_section_height - + top_section_height) + +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]) + + +"""Fonts handling""" +fontpath = path+'fonts/' +NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-' +NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed' +weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf' + +if language in ['ja','zh','zh_tw','ko']: + default = ImageFont.truetype(NotoSansCJK+'Light.otf', 18) + semi = ImageFont.truetype(NotoSansCJK+'DemiLight.otf', 18) + bold = ImageFont.truetype(NotoSansCJK+'Regular.otf', 18) + month_font = ImageFont.truetype(NotoSansCJK+'DemiLight.otf', 40) + +else: + default = ImageFont.truetype(NotoSans+'Light.ttf', 18) + semi = ImageFont.truetype(NotoSans+'.ttf', 18) + bold = ImageFont.truetype(NotoSans+'Medium.ttf', 18) + month_font = ImageFont.truetype(NotoSans+'Light.ttf', 40) + +w_font = ImageFont.truetype(weatherfont, 10) + +x_padding = int((display_width % 10) // 2) +line_height = default.getsize('hg')[1] +line_width = display_width- x_padding + + + +image = Image.new('RGB', (display_width, display_height), 'white') +#def main(): +def write_text(box_width, box_height, text, tuple, + font=default, alignment='middle', adapt_fontsize = False): + text_width, text_height = font.getsize(text) + if adapt_fontsize == True: + size = 10 + while text_width < box_width and text_height < box_height: + size += 1 + font = ImageFont.truetype(font.path, size) + text_width, text_height = font.getsize(text) + + while (text_width, text_height) > (box_width, box_height): + text=text[0:-1] + text_width, text_height = font.getsize(text) + if alignment is "" or "middle" or None: + x = int((box_width / 2) - (text_width / 2)) + if alignment is 'left': + x = 0 + y = int((box_height / 2) - (text_height / 1.7)) + space = Image.new('RGB', (box_width, box_height), color='white') + ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) + image.paste(space, tuple) + +"""Custom function to display longer text into multiple lines (wrapping)""" +def text_wrap(text, font=default, line_width = display_width): + counter, padding = 0, 60 + 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 + + + + +"""Check if internet is available by trying to reach google""" +def internet_available(): + try: + urlopen('https://google.com',timeout=5) + return True + except URLError as err: + return False + +'''Get system timezone and set timezone accordingly''' +def get_tz(): + 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): + 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 reduce_colours(image): + buffer = np.array(image) + r,g,b = buffer[:,:,0], buffer[:,:,1], buffer[:,:,2] + + if display_colours == "bwr": + buffer[np.logical_and(r > 245, g > 245)] = [255,255,255] #white + buffer[np.logical_and(r > 245, g < 245)] = [255,0,0] #red + buffer[np.logical_and(r != 255, r == g )] = [0,0,0] #black + else: + buffer[np.logical_and(r > 245, g > 245)] = [255,255,255] #white + buffer[g < 255] = [0,0,0] #black + + image = Image.fromarray(buffer).rotate(270, expand=True) + return image + +def calibrate(cycles): + """Function for Calibration""" + import e_paper_drivers + epd = e_paper_drivers.EPD() + print('----------Started calibration of E-Paper display----------') + + for i in range(cycles): + epd.init() + print('Calibrating black...') + epd.display_frame(epd.get_frame_buffer(black)) + if display_colours == "bwr": + print('calibrating red...') + epd.display_frame(epd.get_frame_buffer(red)) + print('Calibrating white...') + epd.display_frame(epd.get_frame_buffer(white)) + epd.sleep() + print('Cycle {0} of {1} complete'.format(i, cycle)) + print('-----------Calibration complete----------') + +#if __name__ == '__main__': + #main() + diff --git a/Inky-Calendar/modules/inkycal_agenda.py b/Inky-Calendar/modules/inkycal_agenda.py new file mode 100644 index 0000000..620d73b --- /dev/null +++ b/Inky-Calendar/modules/inkycal_agenda.py @@ -0,0 +1,87 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Calendar module for Inky-Calendar Project + +Copyright by aceisace +""" +from __future__ import print_function +from inkycal_icalendar import upcoming_events +from configuration import * +from settings import * +import arrow + + +"""Find max number of lines that can fit in the middle section and allocate +a position for each line""" +lines = middle_section_height // line_height +line_pos = {} +for i in range(lines): + y = top_section_height + i * line_height + line_pos['pos'+str(i+1)] = (x_padding, y) + + +"""Create a list of dictionaries containing dates of the next days""" +now = arrow.now() +agenda_list = [{'date':now.replace(days=+i), + 'date_str':now.replace(days=+i).format('ddd D MMM YY',locale=language), + 'type':'date'} for i in range(lines)] + + +"""Copy the list from the icalendar module""" +filtered_events = upcoming_events.copy() + +"""Print events with some styling""" +""" +style = 'D MMM YY HH:mm' +if filtered_events: + line_width = max(len(i.name) for i in filtered_events) + for events in filtered_events: + print('{0} {1} | {2} | {3} |'.format(events.name, + ' '* (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: + events.end = events.end.to(get_tz()) + events.begin = events.begin.to(get_tz()) + if hours == '24': + agenda_list.append({'date': events.begin, + 'title':events.begin.format('HH:mm')+' '+ str(events.name), + 'type':'timed_event'}) + if hours == '12': + agenda_list.append({'date': events.begin, + 'title':events.begin.format('hh:mm a')+' '+str(events.name), + 'type':'timed_event'}) + else: + if events.duration.days == 1: + agenda_list.append({'date': events.begin,'title':events.name, 'type':'full_day_event'}) + else: + for days in range(events.duration.days): + agenda_list.append({'date': events.begin.replace(days=+i),'title':events.name, 'type':'full_day_event'}) + +"""Sort events and dates in chronological order""" +agenda_list = sorted(agenda_list, key = lambda i: i['date']) + +"""Crop the agenda_list in case it's too long""" +if len(agenda_list) > len(line_pos): + del agenda_list[len(line_pos):] + +"""Display all events and dates on the display""" +for i in range(len(agenda_list)): + if agenda_list[i]['type'] == 'date': + write_text(line_width, line_height, agenda_list[i]['date_str'], + line_pos['pos'+str(i+1)], alignment = 'left') + elif agenda_list[i]['type'] is 'timed_event': + write_text(line_width, line_height, agenda_list[i]['title'], + line_pos['pos'+str(i+1)], alignment = 'left') + else: + write_text(line_width, line_height, agenda_list[i]['title'], + line_pos['pos'+str(i+1)]) + +"""Crop the image to show only the middle section""" +image.crop((0, top_section_height, display_width, + display_height-bottom_section_height)).save('agenda.png') diff --git a/Inky-Calendar/modules/inkycal_calendar.py b/Inky-Calendar/modules/inkycal_calendar.py new file mode 100644 index 0000000..40440d2 --- /dev/null +++ b/Inky-Calendar/modules/inkycal_calendar.py @@ -0,0 +1,98 @@ +#!/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 datetime +from PIL import Image, ImageDraw + +"""Define some parameters for the grid""" +grid_width, grid_height = display_width, 324 +grid_rows = 6 +grid_coloums = 7 + +padding_left = int((display_width % grid_coloums) / 2) +padding_up = int((grid_height % grid_rows) / 2) +icon_width = grid_width // grid_coloums +icon_height = grid_height // grid_rows + +weekdays_height = 22 +#def main(): +this = datetime.datetime.now() + +"""Add grid-coordinates in the grid dictionary for a later lookup""" +grid = {} + +counter = 0 +for row in range(grid_rows): + y = middle_section_offset - grid_height + row*icon_height + for col in range(grid_coloums): + x = padding_left + col*icon_width + counter += 1 + grid['pos'+str(counter)] = (x,y) + + +"""Set the Calendar to start on the day specified by the settings file """ +if week_starts_on is "" or "Monday": + calendar.setfirstweekday(calendar.MONDAY) +else: + calendar.setfirstweekday(calendar.SUNDAY) + +"""Create a scrolling calendar""" +cal = calendar.monthcalendar(this.year, this.month) +current_row = [cal.index(i) for i in cal if this.day in i][0] + +if current_row > 1: + del cal[:current_row-1] + +if len(cal) < grid_rows: + next_month = this + datetime.timedelta(days=30) + cal_next_month = calendar.monthcalendar(next_month.year, next_month.month) + cal.extend(cal_next_month[:grid_rows - len(cal)] + +""" +flatten = lambda z: [x for y in z for x in y] +cal = flatten(cal) +cal_next_month = flatten(cal_next_month) + +cal.extend(cal_next_month) + +num_font= ImageFont.truetype(NotoSansCJK+'Light.otf', 30) +""" + + + + + + +#draw = ImageDraw.Draw(image) # + + + +""" +counter = 0 +for i in range(len(cal)): + counter += 1 + if cal[i] != 0 and counter <= grid_rows*grid_coloums: + write_text(icon_width, icon_height, str(cal[i]), grid['pos'+str(counter)], + font = num_font) + ##if this.day == cal[i]: + ##pos = grid['pos'+str(counter)] + #x = pos[0] + int(icon_width/2) + #y = pos[1] + int(icon_height/2) + #r = int(icon_width * 0.75#coords = (x-r, y-r, x+r, y+r) + #draw.ellipse(coords, fill= 0, outline='black', + #width=3) + +image.crop((0, top_section_height, display_width, + display_height-bottom_section_height)).save('cal.png') + +#if __name__ == '__main__': +# main() +""" diff --git a/Inky-Calendar/modules/inkycal_icalendar.py b/Inky-Calendar/modules/inkycal_icalendar.py new file mode 100644 index 0000000..20b6c9b --- /dev/null +++ b/Inky-Calendar/modules/inkycal_icalendar.py @@ -0,0 +1,53 @@ +#!/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 = True + +"""Set timelines for filtering upcoming events""" +now = arrow.now(tz=get_tz()) +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 = [] +upcoming_events += [events for ical in calendars for events in ical.events + if now <= events.end <= further_future or now <= 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) + +""" 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: + style = 'DD MMM YY HH:mm' #D MMM YY HH:mm + if upcoming_events: + line_width = max(len(i.name) for i in upcoming_events) + for events in upcoming_events: + print('{0} {1} | {2} | {3} |'.format(events.name, + ' '* (line_width - len(events.name)), events.begin.format(style), + events.end.format(style)), events.all_day) + diff --git a/Inky-Calendar/modules/inkycal_rss.py b/Inky-Calendar/modules/inkycal_rss.py new file mode 100644 index 0000000..9ad8392 --- /dev/null +++ b/Inky-Calendar/modules/inkycal_rss.py @@ -0,0 +1,49 @@ +"""Add rss-feeds at the bottom section of the Calendar""" +import feedparser +from random import shuffle +from settings import * +from configuration import * + +"""Find out how many lines can fit at max in the bottom section""" +lines = bottom_section_height // line_height +"""Create and fill a dictionary of the positions of each line""" +line_pos = {} +for i in range(lines): + y = bottom_section_offset + i * line_height + line_pos['pos' + str(i+1)] = (x_padding, y) + +if bottom_section == "RSS" and rss_feeds != []: + """Parse the RSS-feed titles & summaries and save them to a list""" + rss_feed = [] + for feeds in rss_feeds: + text = feedparser.parse(feeds) + for posts in text.entries: + rss_feed.append('{0}: {1}'.format(posts.title, posts.summary)) + del rss_feed[lines:] + shuffle(rss_feed) + + +"""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, counter = [], 0 + + for posts in rss_feed: + wrapped = text_wrap(posts) + counter += len(filtered) + len(wrapped) + if counter < lines: + filtered.append(wrapped) + filtered = flatten(filtered) + + +## for i in lines: # Show line lenght and content of each line +## print(i, ' ' * (line-len(i)),'| height: ',default.getsize(i)[1]) + +"""Write the correctly formatted text on the display""" + for i in range(len(filtered)): + write_text(display_width, default.getsize('hg')[1], + ' '+filtered[i], line_pos['pos'+str(i+1)], alignment= 'left') + + image.crop((0,bottom_section_offset, display_width, display_height)).save( + 'rss.png') + + del filtered, rss_feed diff --git a/Inky-Calendar/modules/inkycal_weather.py b/Inky-Calendar/modules/inkycal_weather.py new file mode 100644 index 0000000..dc3dde7 --- /dev/null +++ b/Inky-Calendar/modules/inkycal_weather.py @@ -0,0 +1,161 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Weather module for Inky-Calendar software. In development... + +To-do: +- make locations of icons and text dynamic +Copyright by aceisace +""" +from __future__ import print_function +import pyowm +from settings import * +from configuration import * +from PIL import Image, ImageDraw, ImageFont +import arrow + +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' + } + + +"""Split top_section into to 2 rows""" +section_height = top_section_height // 2 +section_width = (top_section_width - top_section_height) // 3 + +"""Allocate icon sizes""" +icon_small = section_height +icon_large = top_section_height + +"""Split top section into 4 coloums""" +section1 = 0 +section2 = icon_large + (top_section_width - icon_large) // 3 * 0 +section3 = icon_large + (top_section_width - icon_large) // 3 * 1 +section4 = icon_large + (top_section_width - icon_large) // 3 * 2 + +"""Allocate positions for icons""" +weather_icon_pos = (section1, 0) +wind_icon_pos = (section2, 0) +sun_icon_pos = (section3, 0) +temperature_icon_pos = (section4, 0) +weather_description_pos = (section2, section_height) +humidity_icon_pos = (section4, section_height) + +"""Allocate positions for text""" +next_to = lambda x: (x[0]+ icon_small, x[1]) +icon_offset = section_width - icon_small + +wind_pos = next_to(wind_icon_pos) +temperature_pos = next_to(temperature_icon_pos) +sun_pos = next_to(sun_icon_pos) +humidity_pos = next_to(humidity_icon_pos) +weather_pos = (section2, section_height) + + +#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: + print("Fetching weather data from openweathermap...",end = ' ') + observation = owm.weather_at_place(location) + print("Done") + + weather = observation.get_weather() + weathericon = weather.get_weather_icon_name() + Humidity = str(weather.get_humidity()) + cloudstatus = str(weather.get_clouds()) + weather_description = (str(weather.get_detailed_status())) + + """Add the icons at the correct positions""" + print('Adding weather info and icons to the image...', end = ' ') + write_text(icon_small, icon_small, '\uf055', temperature_icon_pos, + font = w_font, adapt_fontsize = True) # Temperature icon + + write_text(icon_large, icon_large, weathericons[weathericon], + weather_icon_pos, font = w_font, adapt_fontsize = True) # Weather icon + + write_text(icon_small, icon_small, '\uf07a', humidity_icon_pos, font = w_font, + adapt_fontsize = True) #Humidity icon + + write_text(icon_small,icon_small, '\uf050', wind_icon_pos, font = w_font, + adapt_fontsize = True) #Wind icon + + """Format and write the temperature and windspeed""" + if units == "metric": + Temperature = str(int(weather.get_temperature(unit='celsius')['temp'])) + windspeed = str(int(weather.get_wind()['speed'])) + + write_text(icon_offset, section_height, Temperature+'°C', temperature_pos) + + write_text(icon_offset,section_height, windspeed+" km/h", wind_pos) + + else: + Temperature = str(int(weather.get_temperature('fahrenheit')['temp'])) + windspeed = str(int(weather.get_wind()['speed']*0.621)) + + write_text(icon_offset, section_height, Temperature+' F', temperature_pos) + + write_text(icon_offset,section_height, windspeed+" mph", wind_pos) + + """write the humidity at the given position""" + write_text(icon_offset, section_height, Humidity+'%', humidity_pos) + + now = arrow.now(tz=get_tz()) + sunrise = arrow.get(weather.get_sunrise_time()).to(get_tz()) + sunset = arrow.get(weather.get_sunset_time()).to(get_tz()) + + """Add the sunrise/sunset icon and display the time""" + if (now <= sunrise and now <= sunset) or (now >= sunrise and now >= sunset): + write_text(icon_small, icon_small, '\uf051', sun_icon_pos, font = w_font, + adapt_fontsize = True) + if hours == "24": + write_text(icon_offset, section_height, sunrise.format('H:mm'), sun_pos) + else: + write_text(icon_offset, section_height, sunrise.format('h:mm'), sun_pos) + else: + write_text(icon_small, '\uf052', sun_icon_pos, font = w_font, + adapt_fontsize = True) + if hours == "24": + write_text(icon_offset, section_height, sunset.format('H:mm'), sun_pos) + else: + write_text(icon_offset, section_height, sunset.format('h:mm'), sun_pos) + + + """Add a short weather description""" + write_text(section2+section3-icon_offset, section_height, + weather_description, weather_pos) + + print('Done'+'\n') + + """Show the fetched weather data""" + print("Today's weather report: The current Temperature is {0}°C. The " + "relative humidity is {1} %. The current windspeed is {2} km/h. " + "The sunrise today was at {3}. The sunset is at {4}. The weather can " + "be described with: {5}".format(Temperature, Humidity, windspeed, + sunrise.format('H:mm'), sunset.format('H:mm'), weather_description)) + + image.crop((0,0, top_section_width, top_section_height)).save('weather.png') + + 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_large, icon_large, '\uf07b', weather_icon_pos, + font = w_font, adapt_fontsize = True) + pass + + +#if __name__ == '__main__': + #main() diff --git a/Inky-Calendar/modules/rss.png b/Inky-Calendar/modules/rss.png new file mode 100644 index 0000000..6752235 Binary files /dev/null and b/Inky-Calendar/modules/rss.png differ diff --git a/Inky-Calendar/modules/settings.py b/Inky-Calendar/modules/settings.py new file mode 100644 index 0000000..402d5db --- /dev/null +++ b/Inky-Calendar/modules/settings.py @@ -0,0 +1,18 @@ +ical_urls = ["https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics", +"https://calendar.google.com/calendar/ical/ohcobp9hs097e9nnbppks7blv4%40group.calendar.google.com/private-55859b2165097102e0c061e978eb4926/basic.ics", +"https://calendar.yahoo.com/saadnaseer63/3ac8573af0f38b65367a4e3d287bf06d/ycal.ics?id=1407"] +rss_feeds = ["http://feeds.bbci.co.uk/news/world/rss.xml#"] +update_interval = "60" +api_key = "57c07b8f2ae09e348d32317f1bfe3f52" +location = "Stuttgart, DE" +week_starts_on = "Monday" +events_max_range = "60" +calibration_hours = [0,12,18] +display_colours = "bwr" +language = "en" +units = "metric" +hours = "24" +top_section = "Weather" +middle_section = "Agenda" +bottom_section = "RSS" +show_last_update_time = "False" diff --git a/Inky-Calendar/modules/t_wrapper.py b/Inky-Calendar/modules/t_wrapper.py new file mode 100644 index 0000000..470319b --- /dev/null +++ b/Inky-Calendar/modules/t_wrapper.py @@ -0,0 +1,48 @@ +from configuration import * +from settings import * + +""" +def wrapper(text, font=default, max_width = display_width): + counter = 0 + padding = 50 + lines = [] + if font.getsize(text)[0] < max_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] < max_width- padding: + lines.append(line) + line = '' + counter = i + if i == len(text.split()) and line != '': + lines.append(line) + return lines +""" + +def text_wrap(text, font=default, line_width = display_width): + counter, lines = 0, [] + if font.getsize(text)[0] < line_width: + lines.append(text) + else: + while font.getsize(text)[0] < line_width: + + """ + for i in range(1, len(text.split())+1): + line = ' '.join(text.split()[counter:i]) + print(line, font.getsize(line)[0]) + """ + +#text = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.' + +#text = 'Russia submersible fire was in battery compartment Fourteen crew died in the fire on board' + +text = "Russian LGBT activist Yelena Grigoryeva murdered in St Petersburg: Yelena Grigoryeva, 41, was stabbed and strangled near her home in St Petersburg, relatives say." + +lines = text_wrap(text, default, display_width) + +line = len(max(lines, key=len)) + +for i in lines: + print(i, ' ' * (line-len(i)),'| width: ',default.getsize(i)[0]) + diff --git a/Inky-Calendar/modules/weather.png b/Inky-Calendar/modules/weather.png new file mode 100644 index 0000000..3591225 Binary files /dev/null and b/Inky-Calendar/modules/weather.png differ diff --git a/Inky-Calendar/settings/configuration.py b/Inky-Calendar/settings/configuration.py new file mode 100644 index 0000000..bb21264 --- /dev/null +++ b/Inky-Calendar/settings/configuration.py @@ -0,0 +1,171 @@ +""" +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 +from urllib.request import urlopen +from settings import language +from pytz import timezone +import numpy as np +import os + +"""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.10) +middle_section_height = int(display_height*0.65) +bottom_section_height = int(display_height - middle_section_height - + top_section_height) + +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]) + + +"""Fonts handling""" +fontpath = path+'fonts/' +NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-' +NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed' +weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf' + +if language in ['ja','zh','zh_tw','ko']: + default = ImageFont.truetype(NotoSansCJK+'Light.otf', 18) + semi = ImageFont.truetype(NotoSansCJK+'DemiLight.otf', 18) + bold = ImageFont.truetype(NotoSansCJK+'Regular.otf', 18) + month_font = ImageFont.truetype(NotoSansCJK+'DemiLight.otf', 40) + +else: + default = ImageFont.truetype(NotoSans+'Light.ttf', 18) + semi = ImageFont.truetype(NotoSans+'.ttf', 18) + bold = ImageFont.truetype(NotoSans+'Medium.ttf', 18) + month_font = ImageFont.truetype(NotoSans+'Light.ttf', 40) + +w_font = ImageFont.truetype(weatherfont, 10) + +x_padding = int((display_width % 10) // 2) +line_height = default.getsize('hg')[1] +line_width = display_width- x_padding + + + +image = Image.new('RGB', (display_width, display_height), 'white') +#def main(): +def write_text(box_width, box_height, text, tuple, + font=default, alignment='middle', adapt_fontsize = False): + text_width, text_height = font.getsize(text) + if adapt_fontsize == True: + size = 10 + while text_width < box_width and text_height < box_height: + size += 1 + font = ImageFont.truetype(font.path, size) + text_width, text_height = font.getsize(text) + + while (text_width, text_height) > (box_width, box_height): + text=text[0:-1] + text_width, text_height = font.getsize(text) + if alignment is "" or "middle" or None: + x = int((box_width / 2) - (text_width / 2)) + if alignment is 'left': + x = 0 + y = int((box_height / 2) - (text_height / 1.7)) + space = Image.new('RGB', (box_width, box_height), color='white') + ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) + image.paste(space, tuple) + +"""Custom function to display longer text into multiple lines (wrapping)""" +def text_wrap(text, font=default, line_width = display_width): + counter, padding = 0, 60 + 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 + + + + +"""Check if internet is available by trying to reach google""" +def internet_available(): + try: + urlopen('https://google.com',timeout=5) + return True + except URLError as err: + return False + +'''Get system timezone and set timezone accordingly''' +def get_tz(): + 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): + 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 reduce_colours(image): + buffer = np.array(image) + r,g,b = buffer[:,:,0], buffer[:,:,1], buffer[:,:,2] + + if display_colours == "bwr": + buffer[np.logical_and(r > 245, g > 245)] = [255,255,255] #white + buffer[np.logical_and(r > 245, g < 245)] = [255,0,0] #red + buffer[np.logical_and(r != 255, r == g )] = [0,0,0] #black + else: + buffer[np.logical_and(r > 245, g > 245)] = [255,255,255] #white + buffer[g < 255] = [0,0,0] #black + + image = Image.fromarray(buffer).rotate(270, expand=True) + return image + +def calibrate(cycles): + """Function for Calibration""" + import e_paper_drivers + epd = e_paper_drivers.EPD() + print('----------Started calibration of E-Paper display----------') + + for i in range(cycles): + epd.init() + print('Calibrating black...') + epd.display_frame(epd.get_frame_buffer(black)) + if display_colours == "bwr": + print('calibrating red...') + epd.display_frame(epd.get_frame_buffer(red)) + print('Calibrating white...') + epd.display_frame(epd.get_frame_buffer(white)) + epd.sleep() + print('Cycle {0} of {1} complete'.format(i, cycle)) + print('-----------Calibration complete----------') + +#if __name__ == '__main__': + #main() + diff --git a/Inky-Calendar/settings/settings.py b/Inky-Calendar/settings/settings.py new file mode 100644 index 0000000..4e82abc --- /dev/null +++ b/Inky-Calendar/settings/settings.py @@ -0,0 +1,15 @@ +ical_urls = [""] +update_interval = "60" +api_key = "" +location = "Stuttgart, DE" +week_starts_on = "Monday" +events_max_range = "60" +calibration_hours = [0,12,18] +display_colours = "bwr" +language = "en" +units = "metric" +hours = "24" +top_section = "Weather" +middle_section = "Agenda" +bottom_section = "RSS" +show_last_update_time = "False"