From 15d37b64f0b3dfe8a123c1feae5f18d4321febec Mon Sep 17 00:00:00 2001 From: Ace Date: Mon, 11 Mar 2019 18:12:15 +0100 Subject: [PATCH] New update with lots of improvements Added RSS-feedparser. Added support for user-defined display-update intervals. Removed support for recurring events (temporarily). Imporved processing time for the generated image form nearly 30 seconds to less than a second. Improved readability of docstrings. --- Calendar/E-Paper.py | 286 ++++++++++++++++++++++++++------------------ 1 file changed, 169 insertions(+), 117 deletions(-) diff --git a/Calendar/E-Paper.py b/Calendar/E-Paper.py index 372d79d..9ad3a75 100644 --- a/Calendar/E-Paper.py +++ b/Calendar/E-Paper.py @@ -14,6 +14,20 @@ from time import sleep from dateutil.rrule import * from dateutil.parser import parse import re +import random +import gc + +try: + import feedparser +except ImportError: + print("Please install feedparser with: sudo pip3 install feedparser") + print("and") + print("pip3 install feedparser") + +try: + import numpy as np +except ImportError: + print("Please install numpy with: sudo apt-get install python-numpy") from settings import * from icon_positions_locations import * @@ -41,42 +55,58 @@ EPD_HEIGHT = 384 font = ImageFont.truetype(path+'Assistant-Regular.ttf', 18) im_open = Image.open +possible_update_values = [10, 15, 20, 30, 60] +if int(update_interval) not in possible_update_values: + print('Selected update-interval: ',update_interval, 'minutes') + print('Please select an update interval from these values:', possible_update_values) + raise ValueError + """Main loop starts from here""" def main(): + calibration_countdown = 60//int(update_interval) - 60//int(datetime.now().strftime("%M")) while True: time = datetime.now() hour = int(time.strftime("%-H")) month = int(time.now().strftime('%-m')) year = int(time.now().strftime('%Y')) + mins = int(time.strftime("%M")) + seconds = int(time.strftime("%S")) for i in range(1): print('_________Starting new loop___________'+'\n') - """At the following hours (midnight, midday and 6 pm), perform - a calibration of the display's colours""" - if hour is 0 or hour is 12 or hour is 18: - calibration() + """Start by printing the date and time for easier debugging""" + print('Date:', time.strftime('%a %-d %b %y'), 'Time: '+time.strftime('%H:%M')+'\n') - print('Date:', time.strftime('%a %-d %b %y')+', Time: '+time.strftime('%H:%M')+'\n') + """At the hours specified in the settings file, + calibrate the display to prevent ghosting""" + if hour in calibration_hours: + print('Current countdown:',calibration_countdown) + calibration_countdown -= 1 + print('counts left until calibration:',calibration_countdown) + if calibration_countdown == 1: + calibration() + print('Resetting Countdown') + calibration_countdown = 60//int(update_interval) + print('Calibration countdown:',calibration_countdown) - """Create a blank white page, for debugging, change mode to - to 'RGB' and and save the image by uncommenting the image.save - line at the bottom""" + """Create a blank white page first""" image = Image.new('RGB', (EPD_HEIGHT, EPD_WIDTH), 'white') - """Draw the icon with the current month's name""" + """Add the icon with the current month's name""" image.paste(im_open(mpath+str(time.strftime("%B")+'.jpeg')), monthplace) - """Draw a line seperating the weather and Calendar section""" + """Add the line seperating the weather and Calendar section""" image.paste(seperator, seperatorplace) - """Draw the icons with the weekday-names (Mon, Tue...) and - draw a circle on the current weekday""" + """Add weekday-icons (Mon, Tue...) and draw a circle on the + current weekday""" if (week_starts_on == "Monday"): calendar.setfirstweekday(calendar.MONDAY) image.paste(weekmon, weekplace) image.paste(weekday, weekdaysmon[(time.strftime("%a"))], weekday) + """For those whose week starts on Sunday, change accordingly""" if (week_starts_on == "Sunday"): calendar.setfirstweekday(calendar.SUNDAY) image.paste(weeksun, weekplace) @@ -85,7 +115,6 @@ def main(): """Using the built-in calendar function, draw icons for each number of the month (1,2,3,...28,29,30)""" cal = calendar.monthcalendar(time.year, time.month) - #print(cal) #-uncomment for debugging with incorrect dates for numbers in cal[0]: image.paste(im_open(dpath+str(numbers)+'.jpeg'), positions['a'+str(cal[0].index(numbers)+1)]) @@ -115,7 +144,7 @@ def main(): ImageDraw.Draw(space).text((x, y), text, fill='black', font=font) image.paste(space, tuple) - """ Handling Openweathermap API""" + """Connect to Openweathermap API to fetch weather data""" print("Connecting to Openweathermap API servers...") owm = pyowm.OWM(api_key) if owm.is_API_online() is True: @@ -149,7 +178,7 @@ def main(): print('Temperature: '+Temperature+' °C') print('Humidity: '+Humidity+'%') - print('Icon code: '+weathericon) + #print('Icon code: '+weathericon) print('weather-icon name: '+weathericons[weathericon]) print('Wind speed: '+windspeed+'km/h') print('Sunrise-time: '+sunrisetime) @@ -157,47 +186,51 @@ def main(): print('Cloudiness: ' + cloudstatus+'%') print('Weather description: '+weather_description+'\n') - """Drawing the fetched weather icon""" + """Add the weather icon at the top left corner""" image.paste(im_open(wpath+weathericons[weathericon]+'.jpeg'), wiconplace) - """Drawing the fetched temperature""" + """Add the temperature icon at it's position""" image.paste(tempicon, tempplace) - """Drawing the fetched humidity""" + """Add the humidity icon and display the humidity""" image.paste(humicon, humplace) write_text(50, 35, Humidity + " %", (334, 35)) - """Drawing the fetched sunrise time""" + """Add the sunrise icon and display the sunrise time""" image.paste(sunriseicon, sunriseplace) write_text(50, 35, sunrisetime, (249, 0)) - """Drawing the fetched sunset time""" + """Add the sunset icon and display the sunrise time""" image.paste(sunseticon, sunsetplace) write_text(50, 35, sunsettime, (249, 35)) - """Drawing the wind icon""" + """Add the wind icon at it's position""" image.paste(windicon, windiconspace) - """Write a short weather description""" + """Add a short weather description""" write_text(144, 35, weather_description, (70, 35)) else: + """If no response was received from the openweathermap + api server, add the cloud with question mark""" image.paste(no_response, wiconplace) - """Filter upcoming events from your iCalendar/s""" + """Algorithm for filtering and sorting events from your + iCalendar/s""" print('Fetching events from your calendar'+'\n') events_this_month = [] upcoming = [] - today = date.today() + + """Create a time span using the events_max_range value (in days) + to filter events in that range""" time_span = today + timedelta(days=int(events_max_range)) for icalendars in ical_urls: decode = str(urlopen(icalendars).read().decode()) fix_e_1 = decode.replace('BEGIN:VALARM\r\nACTION:NONE','BEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:') fix_e_2 = fix_e_1.replace('BEGIN:VALARM\r\nACTION:EMAIL','BEGIN:VALARM\r\nACTION:DISPLAY\r\nDESCRIPTION:') - # uncomment line below to display your calendar in ical format - # print(fix_e_2) + # print(fix_e_2) #print iCal as string ical = Calendar(fix_e_2) for events in ical.events: if events.begin.date().month == today.month: @@ -205,37 +238,13 @@ def main(): events_this_month.append(int((events.begin).format('D'))) if today <= events.begin.date() <= time_span: upcoming.append({'date':events.begin.format('YYYY MM DD'), 'event':events.name}) -## if re.search('RRULE',str(events)) is not None: -## r = re.search('RRULE:(.+?)\n',str(events)) -## r_start = re.search('DTSTART:(.+?)\n',str(events)) -## if time.now().month == 12: -## r_string=(r.group(1).rstrip()+';UNTIL='+'%04d%02d%02d'+'T000000Z') % (time.now().year+1, 1, 1) -## else: -## r_string=(r.group(1).rstrip()+';UNTIL='+'%04d%02d%02d'+'T000000Z') % (time.now().year, time.now().month+1, 1) -## rule=rrulestr(r_string,dtstart=parse(r_start.group(1))) -## for i in rule: -## if i.year == time.now().year and i.month == time.now().month and i.day >= time.now().day: -## upcoming.append({'date':str(time.now().year) + " " + time.now().strftime('%m')+ " " + str(i.day).zfill(2), 'event':events.name}) -## if i.day not in events_this_month: -## events_this_month.append(i.day) -## # uncomment this line to see fetched recurring events -## print ("Appended recurring event: " + events.name + " on " + str(time.now().year) + " " + time.now().strftime('%m')+ " " + str(i.day).zfill(2)) -## else: -## if events.begin.date().month == today.month: -## if int((events.begin).format('D')) not in events_this_month: -## events_this_month.append(int((events.begin).format('D'))) -## if today <= events.begin.date() <= time_span: -## upcoming.append({'date':events.begin.format('YYYY MM DD'), 'event':events.name}) def takeDate(elem): return elem['date'] upcoming.sort(key=takeDate) - # uncomment the following 2 lines to display the fetched events - # from your iCalendar - # print('Upcoming events:') - # print(upcoming) + #print('Upcoming events:',upcoming) #Display fetched events def write_text_left(box_width, box_height, text, tuple): text_width, text_height = font.getsize(text) @@ -248,23 +257,77 @@ def main(): image.paste(space, tuple) """Write event dates and names on the E-Paper""" - if len(cal) == 5: - del upcoming[6:] + if additional_feature == "events": + if len(cal) == 5: + del upcoming[6:] - for dates in range(len(upcoming)): - readable_date = datetime.strptime(upcoming[dates]['date'], '%Y %m %d').strftime('%-d %b') - write_text(70, 25, readable_date, date_positions['d'+str(dates+1)]) - for events in range(len(upcoming)): - write_text_left(314, 25, (upcoming[events]['event']), event_positions['e'+str(events+1)]) + for dates in range(len(upcoming)): + readable_date = datetime.strptime(upcoming[dates]['date'], '%Y %m %d').strftime('%-d %b') + write_text(70, 25, readable_date, date_positions['d'+str(dates+1)]) + for events in range(len(upcoming)): + write_text_left(314, 25, (upcoming[events]['event']), event_positions['e'+str(events+1)]) - if len(cal) == 6: - del upcoming[4:] + if len(cal) == 6: + del upcoming[4:] - for dates in range(len(upcoming)): - readable_date = datetime.strptime(upcoming[dates]['date'], '%Y %m %d').strftime('%-d %b') - write_text(70, 25, readable_date, date_positions['d'+str(dates+3)]) - for events in range(len(upcoming)): - write_text_left(314, 25, (upcoming[events]['event']), event_positions['e'+str(events+3)]) + for dates in range(len(upcoming)): + readable_date = datetime.strptime(upcoming[dates]['date'], '%Y %m %d').strftime('%-d %b') + write_text(70, 25, readable_date, date_positions['d'+str(dates+3)]) + for events in range(len(upcoming)): + write_text_left(314, 25, (upcoming[events]['event']), event_positions['e'+str(events+3)]) + + """Add rss-feeds at the bottom section of the Calendar""" + if additional_feature == "rss": + + def multiline_text(text, max_width): + lines = [] + if font.getsize(text)[0] <= max_width: + lines.append(text) + else: + words = text.split(' ') + i = 0 + while i < len(words): + line = '' + while i < len(words) and font.getsize(line + words[i])[0] <= max_width: + line = line + words[i] + " " + i += 1 + if not line: + line = words[i] + i += 1 + lines.append(line) + return lines + + rss_feed = [] + for feeds in rss_feeds: + text = feedparser.parse(feeds) + for posts in text.entries: + rss_feed.append(posts.title) + + random.shuffle(rss_feed) + news = [] + + if len(cal) == 5: + del rss_feed[:6] + + if len(cal) == 6: + del rss_feed[:4] + + for title in range(len(rss_feeds)): + news.append(multiline_text(rss_feed[title], 384)) + + news = [j for i in news for j in i] + + if len(cal) == 5: + if len(news) > 6: + del news[6:] + for lines in range(len(news)): + write_text_left(384, 25, news[lines], rss_places['line_'+str(lines+1)]) + + if len(cal) == 6: + if len(news) > 4: + del news[4:] + for lines in range(len(news)): + write_text_left(384, 25, news[lines], rss_places['line_'+str(lines+3)]) """Draw smaller squares on days with events""" for numbers in events_this_month: @@ -299,64 +362,53 @@ def main(): image.paste(dateicon, positions['f'+str(cal[5].index(today)+1)], dateicon) """ - The function below will take care of displaying the image correctly on E-Paper-Displays. It works by analysing - 2 bands of each pixel (for example, red and green) and re-writes the values in a way so the image only contains - 3 colours, without anytyhing in between. As a result, the image looks much more better on E-Paper. - Currently in beta-phase. + Map all pixels of the generated image to red, white and black + so that the image can be displayed 'correctly' on the E-Paper """ + buffer = np.array(image) + r,g,b = buffer[:,:,0], buffer[:,:,1], buffer[:,:,2] + if display_colours == "bwr": + buffer[np.logical_and(r > 240, g > 240)] = [255,255,255] #white + buffer[np.logical_and(r > 240, g < 240)] = [255,0,0] #red + buffer[np.logical_and(r != 255, r ==g )] = [0,0,0] #black - def display_corrected_image(image): - print('Improving the colours of the generated image.') - # Uncomment following line to save the unprocessed image - # image.save(path+'before.bmp') - width, height = image.size - pixels = image.load() - if display_colours == "bwr": - for x in range(width): - for y in range(height): - pixel = image.getpixel((x, y)) - red = pixel[0] - green = pixel[1] - if red > 240 and green > 240: #white - pixels[x, y] = (255, 255, 255) - elif red > 250 and green < 180: #red - pixels[x, y] = (255, 0, 0) - else: - pixels[x, y] = (0, 0, 0) - - if display_colours == "bw": - for x in range(width): - for y in range(height): - pixel = image.getpixel((x, y)) - red = pixel[0] - green = pixel[1] - if red > 240 and green > 240: #white - pixels[x, y] = (255, 255, 255) - else: - pixels[x, y] = (0, 0, 0) - print('Conversion finished. Enjoy a crisp image on the E-Paper') + if display_colours == "bw": + buffer[np.logical_and(r > 240, g > 240)] = [255,255,255] #white + buffer[r < 240] = [0,0,0] #black + + improved_image = Image.fromarray(buffer).rotate(270, expand=True) + print('Initialising E-Paper Display') + epd.init() + sleep(5) + print('Converting image to data and sending it to the display') + epd.display_frame(epd.get_frame_buffer(improved_image)) + print('Data sent successfully') + print('______Powering off the E-Paper until the next loop______'+'\n') + epd.sleep() - # Uncomment following line to save the processed image - # image.save(path+'after.bmp') - - print('Initialising E-Paper Display') - epd.init() - sleep(5) - print('Converting image to data and sending it to the display') - print('This will take about a minute...'+'\n') - epd.display_frame(epd.get_frame_buffer(image.rotate(270, expand=1))) - print('Data sent successfully') - print('______Powering off the E-Paper until the next loop______'+'\n') - epd.sleep() - del image - - display_corrected_image(image) del events_this_month[:] del upcoming[:] - + del rss_feed[:] + del news[:] + del buffer + del image + del improved_image + gc.collect() + for i in range(1): - nexthour = ((60 - int(time.strftime("%-M")))*60) - (int(time.strftime("%-S"))) - sleep(nexthour) + timings = [] + updates_per_hour = 60//int(update_interval) + + for times in range(updates_per_hour): + interval = 60-(times*int(update_interval)) + if interval >= mins: + time_left = (mins-interval)*(-1) + timings.append(time_left) + next_update = min(timings)*60 + (60-seconds) + print(min(timings),'Minutes and ', (60-seconds),'Seconds left until next loop') + del timings + print('sleeping for',next_update, 'seconds') + sleep(next_update) if __name__ == '__main__': main()