Improved folder structure

With the new folder structure, users can now easily clone the repo with git clone. The need for renaming and shifting folders is now gone.
This commit is contained in:
Ace
2019-12-12 22:52:57 +01:00
committed by GitHub
parent cfdd691213
commit 9517017365
22 changed files with 1726 additions and 0 deletions

1
modules/init.py Normal file
View File

@@ -0,0 +1 @@
#nothing in here. What did you expect?

118
modules/inkycal.py Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Main script of Inky-Calendar software.
Copyright by aceisace
"""
from __future__ import print_function
from configuration import *
from settings import *
import arrow
from time import sleep
import gc
import inkycal_drivers as drivers
import inkycal_rss as rss
import inkycal_weather as weather
import inkycal_calendar as calendar
import inkycal_agenda as agenda
display = drivers.EPD()
skip_calibration = False
"""Perepare for execution of main programm"""
calibration_countdown = 'initial'
image_cleanup()
"""Check time and calibrate display if time """
while True:
now = arrow.now(tz=get_tz())
for _ in range(1):
image = Image.new('RGB', (display_width, display_height), background_colour)
"""------------------Add short info------------------"""
print('Current Date: {0} \nCurrent Time: {1}'.format(now.format(
'D MMM YYYY'), now.format('HH:mm')))
print('-----------Main programm started now----------')
"""------------------Calibration check----------------"""
if skip_calibration != True:
print('Calibration..', end = ' ')
if now.hour in calibration_hours:
if calibration_countdown == 'initial':
print('required. Performing calibration now.')
calibration_countdown = 0
display.calibrate_display(3)
else:
if calibration_countdown % (60 // int(update_interval)) == 0:
display.calibrate_display(3)
calibration_countdown = 0
else:
print('not required. Continuing...')
else:
print('Calibration skipped!. Please note that not calibrating e-paper',
'displays causes ghosting')
"""----------------Generating and assembling images------"""
if top_section == 'Weather':
try:
weather.main()
weather_image = Image.open(image_path + 'weather.png')
image.paste(weather_image, (0, 0))
except:
pass
if middle_section == 'Calendar':
try:
calendar.main()
calendar_image = Image.open(image_path + 'calendar.png')
image.paste(calendar_image, (0, middle_section_offset))
except:
pass
if middle_section == 'Agenda':
try:
agenda.main()
agenda_image = Image.open(image_path + 'agenda.png')
image.paste(agenda_image, (0, middle_section_offset))
except:
pass
if bottom_section == 'RSS':
try:
rss.main()
rss_image = Image.open(image_path + 'rss.png')
image.paste(rss_image, (0, bottom_section_offset))
except:
pass
image.save(image_path + 'canvas.png')
display.reduce_colours(image)
"""---------Refreshing E-Paper with newly created image-----------"""
display.show_image(image)
"""--------------Post processing after main loop-----------------"""
"""Collect some garbage to free up some resources"""
gc.collect()
"""Adjust calibration countdowns"""
if calibration_countdown == 'initial':
calibration_countdown = 0
calibration_countdown += 1
"""Calculate duration until next display refresh"""
for _ in range(1):
update_timings = [(60 - int(update_interval)*updates) for updates in
range(60//int(update_interval))]
minutes = [i - now.minute for i in update_timings if i >= now.minute]
refresh_countdown = minutes[0]*60 + (60 - now.second)
print('{0} Minutes left until next refresh'.format(minutes[0]))
del update_timings, minutes, image
sleep(refresh_countdown)

141
modules/inkycal_agenda.py Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Agenda module for Inky-Calendar Project
Copyright by aceisace
"""
from __future__ import print_function
from inkycal_icalendar import fetch_events
from configuration import*
from settings import *
import arrow
fontsize = 12
show_events = True
print_events = False
style = 'D MMM YY HH:mm'
"""Add a border to increase readability"""
border_top = int(middle_section_height * 0.02)
border_left = int(middle_section_width * 0.02)
"""Choose font optimised for the agenda section"""
font = ImageFont.truetype(NotoSans+'Medium.ttf', fontsize)
line_height = int(font.getsize('hg')[1] * 1.2) + 1
line_width = int(middle_section_width - (border_left*2))
"""Set some positions for events, dates and times"""
date_col_width = int(line_width * 0.20)
time_col_width = int(line_width * 0.15)
event_col_width = int(line_width - date_col_width - time_col_width)
date_col_start = border_left
time_col_start = date_col_start + date_col_width
event_col_start = time_col_start + time_col_width
"""Find max number of lines that can fit in the middle section and allocate
a position for each line"""
if bottom_section:
max_lines = int((middle_section_height - border_top*2) // line_height)
else:
max_lines = int(middle_section_height+bottom_section_height -
(border_top * 2))// line_height
line_pos = [(border_left, int(top_section_height + border_top + line * line_height))
for line in range(max_lines)]
def main():
try:
clear_image('middle_section')
print('Agenda module: Generating image...', end = '')
now = arrow.now(get_tz())
today_start = arrow.get(now.year, now.month, now.day)
"""Create a list of dictionaries containing dates of the next days"""
agenda_events = [{'date':today_start.replace(days=+_),
'date_str': now.replace(days=+_).format('ddd D MMM',locale=language),
'type':'date'} for _ in range(max_lines)]
"""Copy the list from the icalendar module with some conditions"""
upcoming_events = fetch_events()
filtered_events = [events for events in upcoming_events if
events.end > now]
"""Set print_events_to True to print all events in this month"""
if print_events == True and filtered_events:
auto_line_width = max(len(_.name) for _ in filtered_events)
for events in filtered_events:
print('{0} {1} | {2} | {3} | All day ='.format(events.name,
' '* (auto_line_width - len(events.name)), events.begin.format(style),
events.end.format(style)), events.all_day)
"""Convert the event-timings from utc to the specified locale's time
and create a ready-to-display list for the agenda view"""
for events in filtered_events:
if not events.all_day:
agenda_events.append({'date': events.begin, 'time': events.begin.format(
'HH:mm' if hours == '24' else 'hh:mm a'), 'name':str(events.name),
'type':'timed_event'})
else:
if events.duration.days == 1:
agenda_events.append({'date': events.begin,'time':'All day',
'name': events.name,'type':'full_day_event'})
else:
for day in range(events.duration.days):
agenda_events.append({'date': events.begin.replace(days=+day),
'time':'All day','name':events.name, 'type':'full_day_event'})
"""Sort events and dates in chronological order"""
agenda_events = sorted(agenda_events, key = lambda event: event['date'])
"""Crop the agenda_events in case it's too long"""
del agenda_events[max_lines:]
"""Display all events, dates and times on the display"""
if show_events == True:
previous_date = None
for events in range(len(agenda_events)):
if agenda_events[events]['type'] == 'date':
if previous_date == None or previous_date != agenda_events[events][
'date']:
write_text(date_col_width, line_height,
agenda_events[events]['date_str'], line_pos[events], font = font)
previous_date = agenda_events[events]['date']
draw.line((date_col_start, line_pos[events][1],
line_width,line_pos[events][1]), fill = 'red' if display_type == 'colour' else 'black')
elif agenda_events[events]['type'] == 'timed_event':
write_text(time_col_width, line_height, agenda_events[events]['time'],
(time_col_start, line_pos[events][1]), font = font)
write_text(event_col_width, line_height, (''+agenda_events[events][
'name']), (event_col_start, line_pos[events][1]),
alignment = 'left', font = font)
else:
write_text(time_col_width, line_height, agenda_events[events]['time'],
(time_col_start, line_pos[events][1]), font = font)
write_text(event_col_width, line_height, (''+agenda_events[events]['name']),
(event_col_start, line_pos[events][1]), alignment = 'left', font = font)
"""Crop the image to show only the middle section"""
if bottom_section:
agenda_image = crop_image(image, 'middle_section')
else:
agenda_image = image.crop((0,middle_section_offset,display_width, display_height))
agenda_image.save(image_path+'agenda.png')
print('Done')
except Exception as e:
"""If something went wrong, print a Error message on the Terminal"""
print('Failed!')
print('Error in Agenda module!')
print('Reason: ',e)
pass
if __name__ == '__main__':
main()

194
modules/inkycal_calendar.py Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Calendar module for Inky-Calendar Project
Copyright by aceisace
"""
from __future__ import print_function
import calendar
from configuration import *
from settings import *
import arrow
from PIL import Image, ImageDraw
print_events = False
show_events = True
max_event_lines = 4
style = "DD MMM"
event_icon = 'square' # dot #square
if show_events == True:
from inkycal_icalendar import fetch_events
"""Add a border to increase readability"""
border_top = int(middle_section_height * 0.02)
border_left = int(middle_section_width * 0.02)
main_area_height = middle_section_height-border_top*2
main_area_width = middle_section_width-border_left*2
"""Calculate height for each sub-section"""
month_name_height = int(main_area_height*0.1)
weekdays_height = int(main_area_height*0.05)
calendar_height = int(main_area_height*0.6)
events_height = int(main_area_height*0.25)
"""Set rows and coloumns in the calendar section and calculate sizes"""
calendar_rows, calendar_coloumns = 6, 7
icon_width = main_area_width // calendar_coloumns
icon_height = calendar_height // calendar_rows
"""Calculate paddings for calendar section"""
x_padding_calendar = int((main_area_width % icon_width) / 2)
y_padding_calendar = int((main_area_height % calendar_rows) / 2)
"""Add coordinates for number icons inside the calendar section"""
grid_start_y = (middle_section_offset + border_top + month_name_height +
weekdays_height + y_padding_calendar)
grid_start_x = border_left + x_padding_calendar
grid = [(grid_start_x + icon_width*x, grid_start_y + icon_height*y)
for y in range(calendar_rows) for x in range(calendar_coloumns)]
weekday_pos = [(grid_start_x + icon_width*_, middle_section_offset +
month_name_height) for _ in range(calendar_coloumns)]
event_lines = [(border_left,(bottom_section_offset - events_height)+
int(events_height/max_event_lines*_)) for _ in
range(max_event_lines)]
def main():
try:
clear_image('middle_section')
print('Calendar module: Generating image...', end = '')
now = arrow.now(tz = get_tz())
"""Set up the Calendar template based on personal preferences"""
if week_starts_on == "Monday":
calendar.setfirstweekday(calendar.MONDAY)
weekstart = now.replace(days = - now.weekday())
else:
calendar.setfirstweekday(calendar.SUNDAY)
weekstart = now.replace(days = - now.isoweekday())
"""Write the name of the current month at the correct position"""
write_text(main_area_width, month_name_height,
str(now.format('MMMM',locale=language)), (border_left,
middle_section_offset), autofit = True)
"""Set up weeknames in local language and add to main section"""
weekday_names = [weekstart.replace(days=+_).format('ddd',locale=language)
for _ in range(7)]
for _ in range(len(weekday_pos)):
write_text(icon_width, weekdays_height, weekday_names[_],
weekday_pos[_], autofit = True)
"""Create a calendar template and flatten (remove nestings)"""
flatten = lambda z: [x for y in z for x in y]
calendar_flat = flatten(calendar.monthcalendar(now.year, now.month))
"""Add the numbers on the correct positions"""
for i in range(len(calendar_flat)):
if calendar_flat[i] != 0:
write_text(icon_width, icon_height, str(calendar_flat[i]), grid[i])
"""Draw a red/black circle with the current day of month in white"""
icon = Image.new('RGBA', (icon_width, icon_height))
current_day_pos = grid[calendar_flat.index(now.day)]
x_circle,y_circle = int(icon_width/2), int(icon_height/2)
radius = int(icon_width * 0.25)
text_width, text_height = default.getsize(str(now.day))
x_text = int((icon_width / 2) - (text_width / 2))
y_text = int((icon_height / 2) - (text_height / 1.7))
ImageDraw.Draw(icon).ellipse((x_circle-radius, y_circle-radius,
x_circle+radius, y_circle+radius), fill= 'red' if
display_type == 'colour' else 'black', outline=None)
ImageDraw.Draw(icon).text((x_text, y_text), str(now.day), fill='white',
font=bold)
image.paste(icon, current_day_pos, icon)
"""Create some reference points for the current month"""
days_current_month = calendar.monthrange(now.year, now.month)[1]
month_start = now.replace(days =-now.day+1)
month_end = now.replace(days=+(days_current_month-now.day))
if show_events == True:
"""Filter events which begin before the end of this month"""
upcoming_events = fetch_events()
calendar_events = [events for events in upcoming_events if
events.begin < month_end and events.begin.month == now.month]
"""Find days with events in the current month"""
days_with_events = []
for events in calendar_events:
if events.duration.days <= 1:
days_with_events.append(int(events.begin.format('D')))
else:
for day in range(events.duration.days):
days_with_events.append(
int(events.begin.replace(days=+i).format('D')))
days_with_events = set(days_with_events)
if event_icon == 'dot':
for days in days_with_events:
write_text(icon_width, int(icon_height * 0.2), '',
(grid[calendar_flat.index(days)][0],
int(grid[calendar_flat.index(days)][1] + icon_height*0.8)))
if event_icon == 'square':
square_size = int(icon_width *0.6)
center_x = int((icon_width - square_size) / 2)
center_y = int((icon_height - square_size) / 2)
for days in days_with_events:
draw_square((int(grid[calendar_flat.index(days)][0]+center_x),
int(grid[calendar_flat.index(days)][1] + center_y )),
8, square_size , square_size)
"""Add a small section showing events of today and tomorrow"""
event_list = ['{0} at {1} : {2}'.format('today', event.begin.format(
'HH:mm' if hours == 24 else 'hh:mm'), event.name)
for event in calendar_events if event.begin.day == now.day]
event_list += ['{0} at {1} : {2}'.format('tomorrow', event.begin.format(
'HH:mm' if hours == 24 else 'hh:mm'), event.name)
for event in calendar_events if event.begin.day == now.replace(days=+1).day]
del event_list[4:]
if event_list:
for lines in event_list:
write_text(main_area_width, int(events_height/max_event_lines), lines,
event_lines[event_list.index(lines)], alignment='left',
fill_height = 0.7)
else:
write_text(main_area_width, int(events_height/max_event_lines),
'No events today or tomorrow', event_lines[0], alignment='left',
fill_height = 0.7)
"""Set print_events_to True to print all events in this month"""
style = 'DD MMM YY HH:mm'
if print_events == True and calendar_events:
line_width = max(len(_.name) for _ in calendar_events)
for events in calendar_events:
print('{0} {1} | {2} | {3} | All day ='.format(events.name,
' ' * (line_width - len(events.name)), events.begin.format(style),
events.end.format(style)), events.all_day)
calendar_image = crop_image(image, 'middle_section')
calendar_image.save(image_path+'calendar.png')
print('Done')
except Exception as e:
"""If something went wrong, print a Error message on the Terminal"""
print('Failed!')
print('Error in Calendar module!')
print('Reason: ',e)
pass
if __name__ == '__main__':
main()

348
modules/inkycal_drivers.py Normal file
View File

@@ -0,0 +1,348 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Drivers file for Inky-Calendar software.
Handles E-Paper display related tasks
"""
from PIL import Image
import RPi.GPIO as GPIO
from settings import display_type
import numpy
import spidev
import RPi.GPIO as GPIO
from time import sleep
RST_PIN = 17
DC_PIN = 25
CS_PIN = 8
BUSY_PIN = 24
EPD_WIDTH = 640
EPD_HEIGHT = 384
SPI = spidev.SpiDev(0, 0)
def epd_digital_write(pin, value):
GPIO.output(pin, value)
def epd_digital_read(pin):
return GPIO.input(BUSY_PIN)
def epd_delay_ms(delaytime):
sleep(delaytime / 1000.0)
def spi_transfer(data):
SPI.writebytes(data)
def epd_init():
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(RST_PIN, GPIO.OUT)
GPIO.setup(DC_PIN, GPIO.OUT)
GPIO.setup(CS_PIN, GPIO.OUT)
GPIO.setup(BUSY_PIN, GPIO.IN)
SPI.max_speed_hz = 4000000
SPI.mode = 0b00
return 0;
# EPD7IN5 commands
PANEL_SETTING = 0x00
POWER_SETTING = 0x01
POWER_OFF = 0x02
POWER_OFF_SEQUENCE_SETTING = 0x03
POWER_ON = 0x04
POWER_ON_MEASURE = 0x05
BOOSTER_SOFT_START = 0x06
DEEP_SLEEP = 0x07
DATA_START_TRANSMISSION_1 = 0x10
DATA_STOP = 0x11
DISPLAY_REFRESH = 0x12
IMAGE_PROCESS = 0x13
LUT_FOR_VCOM = 0x20
LUT_BLUE = 0x21
LUT_WHITE = 0x22
LUT_GRAY_1 = 0x23
LUT_GRAY_2 = 0x24
LUT_RED_0 = 0x25
LUT_RED_1 = 0x26
LUT_RED_2 = 0x27
LUT_RED_3 = 0x28
LUT_XON = 0x29
PLL_CONTROL = 0x30
TEMPERATURE_SENSOR_COMMAND = 0x40
TEMPERATURE_CALIBRATION = 0x41
TEMPERATURE_SENSOR_WRITE = 0x42
TEMPERATURE_SENSOR_READ = 0x43
VCOM_AND_DATA_INTERVAL_SETTING = 0x50
LOW_POWER_DETECTION = 0x51
TCON_SETTING = 0x60
TCON_RESOLUTION = 0x61
SPI_FLASH_CONTROL = 0x65
REVISION = 0x70
GET_STATUS = 0x71
AUTO_MEASUREMENT_VCOM = 0x80
READ_VCOM_VALUE = 0x81
VCM_DC_SETTING = 0x82
class EPD:
def __init__(self):
self.reset_pin = RST_PIN
self.dc_pin = DC_PIN
self.busy_pin = BUSY_PIN
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
def digital_write(self, pin, value):
epd_digital_write(pin, value)
def digital_read(self, pin):
return epd_digital_read(pin)
def delay_ms(self, delaytime):
epd_delay_ms(delaytime)
def send_command(self, command):
self.digital_write(self.dc_pin, GPIO.LOW)
spi_transfer([command])
def send_data(self, data):
self.digital_write(self.dc_pin, GPIO.HIGH)
spi_transfer([data])
def init(self):
if (epd_init() != 0):
return -1
self.reset()
self.send_command(POWER_SETTING)
self.send_data(0x37)
self.send_data(0x00)
self.send_command(PANEL_SETTING)
self.send_data(0xCF)
self.send_data(0x08)
self.send_command(BOOSTER_SOFT_START)
self.send_data(0xc7)
self.send_data(0xcc)
self.send_data(0x28)
self.send_command(POWER_ON)
self.wait_until_idle()
self.send_command(PLL_CONTROL)
self.send_data(0x3c)
self.send_command(TEMPERATURE_CALIBRATION)
self.send_data(0x00)
self.send_command(VCOM_AND_DATA_INTERVAL_SETTING)
self.send_data(0x77)
self.send_command(TCON_SETTING)
self.send_data(0x22)
self.send_command(TCON_RESOLUTION)
self.send_data(0x02) #source 640
self.send_data(0x80)
self.send_data(0x01) #gate 384
self.send_data(0x80)
self.send_command(VCM_DC_SETTING)
self.send_data(0x1E) #decide by LUT file
self.send_command(0xe5) #FLASH MODE
self.send_data(0x03)
def wait_until_idle(self):
while(self.digital_read(self.busy_pin) == 0): # 0: busy, 1: idle
self.delay_ms(100)
def reset(self):
self.digital_write(self.reset_pin, GPIO.LOW) # module reset
self.delay_ms(200)
self.digital_write(self.reset_pin, GPIO.HIGH)
self.delay_ms(200)
def calibrate_display(self, no_of_cycles):
"""Function for Calibration"""
if display_type == 'colour':
packets = int(self.width / 2 * self.height)
if display_type == 'black_and_white':
packets = int(self.width / 4 * self.height)
white, red, black = 0x33, 0x04, 0x00
self.init()
print('----------Started calibration of E-Paper display----------')
for _ in range(no_of_cycles):
self.send_command(DATA_START_TRANSMISSION_1)
print('Calibrating black...')
[self.send_data(black) for i in range(packets)]
self.send_command(DISPLAY_REFRESH)
self.wait_until_idle()
if display_type == 'colour':
print('Calibrating red...')
self.send_command(DATA_START_TRANSMISSION_1)
[self.send_data(red) for i in range(packets)]
self.send_command(DISPLAY_REFRESH)
self.wait_until_idle()
print('Calibrating white...')
self.send_command(DATA_START_TRANSMISSION_1)
[self.send_data(white) for i in range(packets)]
self.send_command(DISPLAY_REFRESH)
self.wait_until_idle()
print('Cycle {0} of {1} complete'.format(_+1, no_of_cycles))
print('-----------Calibration complete----------')
self.sleep()
def reduce_colours(self, image):
buffer = numpy.array(image)
r,g,b = buffer[:,:,0], buffer[:,:,1], buffer[:,:,2]
if display_type == "colour":
buffer[numpy.logical_and(r <= 180, r == g)] = [0,0,0] #black
buffer[numpy.logical_and(r >= 150, g >= 150)] = [255,255,255] #white
buffer[numpy.logical_and(r >= 150, g <= 90)] = [255,0,0] #red
if display_type == "black_and_white":
buffer[numpy.logical_and(r > 245, g > 245)] = [255,255,255] #white
buffer[g < 255] = [0,0,0] #black
image = Image.fromarray(buffer)
return image
def clear(self, colour='white'):
if display_type == 'colour':
packets = int(self.width / 2 * self.height)
if display_type == 'black_and_white':
packets = int(self.width / 4 * self.height)
if colour == 'white': data = 0x33
if colour == 'red': data = 0x04
if colour == 'black': data = 0x00
self.init()
self.send_command(DATA_START_TRANSMISSION_1)
[self.send_data(data) for _ in range(packets)]
self.send_command(DISPLAY_REFRESH)
print('waiting until E-Paper is not busy')
self.delay_ms(100)
self.wait_until_idle()
print('E-Paper free')
self.sleep()
def get_frame_buffer(self, image):
imwidth, imheight = image.size
if imwidth == self.height and imheight == self.width:
image = image.rotate(270, expand = True)
print('Rotated image by 270 degrees...', end= '')
elif imwidth != self.width or imheight != self.height:
raise ValueError('Image must be same dimensions as display \
({0}x{1}).' .format(self.width, self.height))
else:
print('Image size OK')
imwidth, imheight = image.size
if display_type == 'colour':
buf = [0x00] * int(self.width * self.height / 4)
image_grayscale = image.convert('L', dither=None)
pixels = image_grayscale.load()
for y in range(self.height):
for x in range(self.width):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] == 0: # black
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
elif pixels[x, y] == 76: # convert gray to red
buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2))
buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2)
else: # white
buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2)
if display_type == 'black_and_white':
buf = [0x00] * int(self.width * self.height / 8)
image_monocolor = image.convert('1')
pixels = image_monocolor.load()
for y in range(self.height):
for x in range(self.width):
# Set the bits for the column of pixels at the current position.
if pixels[x, y] != 0:
buf[int((x + y * self.width) / 8)] |= 0x80 >> (x % 8)
return buf
def display_frame(self, frame_buffer):
self.send_command(DATA_START_TRANSMISSION_1)
if display_type == 'colour':
for i in range(0, int(self.width / 4 * self.height)):
temp1 = frame_buffer[i]
j = 0
while (j < 4):
if ((temp1 & 0xC0) == 0xC0):
temp2 = 0x03 #white
elif ((temp1 & 0xC0) == 0x00):
temp2 = 0x00 #black
else:
temp2 = 0x04 #red
temp2 = (temp2 << 4) & 0xFF
temp1 = (temp1 << 2) & 0xFF
j += 1
if((temp1 & 0xC0) == 0xC0):
temp2 |= 0x03 #white
elif ((temp1 & 0xC0) == 0x00):
temp2 |= 0x00 #black
else:
temp2 |= 0x04 #red
temp1 = (temp1 << 2) & 0xFF
self.send_data(temp2)
j += 1
if display_type == 'black_and_white':
for i in range(0, 30720):
temp1 = frame_buffer[i]
j = 0
while (j < 8):
if(temp1 & 0x80):
temp2 = 0x03 #white
else:
temp2 = 0x00 #black
temp2 = (temp2 << 4) & 0xFF
temp1 = (temp1 << 1) & 0xFF
j += 1
if(temp1 & 0x80):
temp2 |= 0x03 #white
else:
temp2 |= 0x00 #black
temp1 = (temp1 << 1) & 0xFF
self.send_data(temp2)
j += 1
self.send_command(DISPLAY_REFRESH)
self.delay_ms(100)
self.wait_until_idle()
def show_image(self, image, reduce_colours = True):
print('Initialising E-Paper Display...', end='')
self.init()
sleep(5)
print('Done')
if reduce_colours == True:
print('Optimising Image for E-Paper displays...', end = '')
image = self.reduce_colours(image)
print('Done')
else:
print('No colour optimisation done on image')
print('Creating image buffer and sending it to E-Paper display...', end='')
data = self.get_frame_buffer(image)
print('Done')
print('Refreshing display...', end = '')
self.display_frame(data)
print('Done')
print('Sending E-Paper to deep sleep mode...',end='')
self.sleep()
print('Done')
def sleep(self):
self.send_command(POWER_OFF)
self.wait_until_idle()
self.send_command(DEEP_SLEEP)
self.send_data(0xa5)

View File

@@ -0,0 +1,59 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
iCalendar (parsing) module for Inky-Calendar Project
Copyright by aceisace
"""
from __future__ import print_function
from configuration import *
from settings import ical_urls
import arrow
from ics import Calendar
print_events = False
style = 'DD MMM YY HH:mm'
def fetch_events():
"""Set timelines for filtering upcoming events"""
now = arrow.now(tz=get_tz())
beginning_of_month = now.replace(days= - now.day +1)
near_future = now.replace(days= 30)
further_future = now.replace(days=40)
"""Parse the iCalendars from the urls, fixing some known errors with ics"""
calendars = [Calendar(fix_ical(url)) for url in ical_urls]
"""Filter any upcoming events from all iCalendars and add them to a list"""
upcoming_events = [events for ical in calendars for events in ical.events
if beginning_of_month <= events.end <= further_future or
beginning_of_month <= events.begin <= near_future]
"""Sort events according to their beginning date"""
def sort_dates(event):
return event.begin
upcoming_events.sort(key=sort_dates)
"""Multiday events are displayed incorrectly; fix that"""
for events in upcoming_events:
if events.all_day and events.duration.days > 1:
events.end = events.end.replace(days=-2)
if not events.all_day:
events.begin = events.begin.to(get_tz())
events.end = events.end.to(get_tz())
""" The list upcoming_events should not be modified. If you need the data from
this one, copy the list or the contents to another one."""
#print(upcoming_events) # Print all events. Might look a bit messy
"""Print upcoming events in a more appealing way"""
if print_events == True and upcoming_events:
line_width = max(len(i.name) for i in upcoming_events)
for events in upcoming_events:
print('{0} {1} | {2} | {3} | All day ='.format(events.name,
' '* (line_width - len(events.name)), events.begin.format(style),
events.end.format(style)), events.all_day)
return upcoming_events

85
modules/inkycal_rss.py Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
RSS module for Inky-Calendar software.
Copyright by aceisace
"""
from __future__ import print_function
import feedparser
from random import shuffle
from settings import *
from configuration import *
fontsize = 14
"""Add a border to increase readability"""
border_top = int(bottom_section_height * 0.05)
border_left = int(bottom_section_width * 0.02)
"""Choose font optimised for the weather section"""
font = ImageFont.truetype(NotoSans+'.ttf', fontsize)
space_between_lines = 1
line_height = font.getsize('hg')[1] + space_between_lines
line_width = bottom_section_width - (border_left*2)
"""Find out how many lines can fit at max in the bottom section"""
max_lines = (bottom_section_height - (border_top*2)) // (font.getsize('hg')[1]
+ space_between_lines)
"""Calculate the height padding so the lines look centralised"""
y_padding = int( (bottom_section_height % line_height) / 2 )
"""Create a list containing positions of each line"""
line_positions = [(border_left, bottom_section_offset +
border_top + y_padding + _*line_height ) for _ in range(max_lines)]
def main():
if bottom_section == "RSS" and rss_feeds != [] and internet_available() == True:
try:
clear_image('bottom_section')
print('RSS module: Connectivity check passed. Generating image...',
end = '')
"""Parse the RSS-feed titles & summaries and save them to a list"""
parsed_feeds = []
for feeds in rss_feeds:
text = feedparser.parse(feeds)
for posts in text.entries:
parsed_feeds.append('{0}: {1}'.format(posts.title, posts.summary))
"""Shuffle the list, then crop it to the max number of lines"""
shuffle(parsed_feeds)
del parsed_feeds[max_lines:]
"""Check the lenght of each feed. Wrap the text if it doesn't fit on one line"""
flatten = lambda z: [x for y in z for x in y]
filtered_feeds, counter = [], 0
for posts in parsed_feeds:
wrapped = text_wrap(posts, font = font, line_width = line_width)
counter += len(filtered_feeds) + len(wrapped)
if counter < max_lines:
filtered_feeds.append(wrapped)
filtered_feeds = flatten(filtered_feeds)
"""Write the correctly formatted text on the display"""
for _ in range(len(filtered_feeds)):
write_text(line_width, line_height, filtered_feeds[_],
line_positions[_], font = font, alignment= 'left')
del filtered_feeds, parsed_feeds
rss_image = crop_image(image, 'bottom_section')
rss_image.save(image_path+'rss.png')
print('Done')
except Exception as e:
"""If something went wrong, print a Error message on the Terminal"""
print('Failed!')
print('Error in RSS module!')
print('Reason: ',e)
pass
if __name__ == '__main__':
main()

354
modules/inkycal_weather.py Normal file
View File

@@ -0,0 +1,354 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Weather module for Inky-Calendar software.
The lunar phase calculation is from Sean B. Palmer, inamidst.com.
Thank You Palmer for the awesome code!
Copyright by aceisace
"""
from __future__ import print_function
import pyowm
from settings import *
from configuration import *
from PIL import Image, ImageDraw, ImageFont
import arrow
import math, decimal
dec = decimal.Decimal
"""Optional parameters"""
round_temperature = True
round_windspeed = True
use_beaufort = True
show_wind_direction = False
use_wind_direction_icon = False
"""Set the optional parameters"""
decimal_places_temperature = None if round_temperature == True else 1
decimal_places_windspeed = None if round_windspeed == True else 1
print('Initialising weather...', end=' ')
owm = pyowm.OWM(api_key, language=language)
print('Done')
"""Icon-code to unicode dictionary for weather-font"""
weathericons = {
'01d': '\uf00d', '02d': '\uf002', '03d': '\uf013',
'04d': '\uf012', '09d': '\uf01a', '10d': '\uf019',
'11d': '\uf01e', '13d': '\uf01b', '50d': '\uf014',
'01n': '\uf02e', '02n': '\uf013', '03n': '\uf013',
'04n': '\uf013', '09n': '\uf037', '10n': '\uf036',
'11n': '\uf03b', '13n': '\uf038', '50n': '\uf023'
}
"""Add a border to increase readability"""
border_top = int(top_section_height * 0.05)
border_left = int(top_section_width * 0.02)
"""Calculate size for each weather sub-section"""
row_height = (top_section_height-(border_top*2)) // 3
coloumn_width = (top_section_width-(border_left*2)) // 7
"""Calculate paddings"""
x_padding = int( (top_section_width % coloumn_width) / 2 )
y_padding = int( (top_section_height % row_height) / 2 )
"""Allocate sizes for weather icons"""
icon_small = row_height
icon_medium = row_height * 2
"""Calculate the x-axis position of each coloumn"""
coloumn1 = x_padding
coloumn2 = coloumn1 + coloumn_width
coloumn3 = coloumn2 + coloumn_width
coloumn4 = coloumn3 + coloumn_width
coloumn5 = coloumn4 + coloumn_width
coloumn6 = coloumn5 + coloumn_width
coloumn7 = coloumn6 + coloumn_width
"""Calculate the y-axis position of each row"""
row1 = y_padding
row2 = row1 + row_height
row3 = row2 + row_height
"""Allocate positions for current weather details"""
text_now_pos = (coloumn1, row1)
weather_icon_now_pos = (coloumn1, row2)
temperature_icon_now_pos = (coloumn2, row1)
temperature_now_pos = (coloumn2+icon_small, row1)
humidity_icon_now_pos = (coloumn2, row2)
humidity_now_pos = (coloumn2+icon_small, row2)
windspeed_icon_now_pos = (coloumn2, row3)
windspeed_now_pos = (coloumn2+icon_small, row3)
moon_phase_now_pos = (coloumn3, row1)
sunrise_icon_now_pos = (coloumn3, row2)
sunrise_time_now_pos = (coloumn3+icon_small, row2)
sunset_icon_now_pos = (coloumn3, row3)
sunset_time_now_pos = (coloumn3+ icon_small, row3)
"""Allocate positions for weather forecast after 3 hours"""
text_fc1_pos = (coloumn4, row1)
icon_fc1_pos = (coloumn4, row2)
temperature_fc1_pos = (coloumn4, row3)
"""Allocate positions for weather forecast after 6 hours"""
text_fc2_pos = (coloumn5, row1)
icon_fc2_pos = (coloumn5, row2)
temperature_fc2_pos = (coloumn5, row3)
"""Allocate positions for weather forecast after 9 hours"""
text_fc3_pos = (coloumn6, row1)
icon_fc3_pos = (coloumn6, row2)
temperature_fc3_pos = (coloumn6, row3)
"""Allocate positions for weather forecast after 12 hours"""
text_fc4_pos = (coloumn7, row1)
icon_fc4_pos = (coloumn7, row2)
temperature_fc4_pos = (coloumn7, row3)
"""Windspeed (m/s) to beaufort (index of list) conversion"""
windspeed_to_beaufort = [0.02, 1.5, 3.3, 5.4, 7.9, 10.7, 13.8, 17.1, 20.7,
24.4, 28.4, 32.6, 100]
def to_units(kelvin):
"""Function to convert tempertures from kelvin to celcius or fahrenheit"""
degrees_celsius = round(kelvin - 273.15, ndigits = decimal_places_temperature)
fahrenheit = round((kelvin - 273.15) * 9/5 + 32,
ndigits = decimal_places_temperature)
if units == 'metric':
conversion = str(degrees_celsius) + '°C'
if units == 'imperial':
conversion = str(fahrenheit) + 'F'
return conversion
def red_temp(negative_temperature):
if display_type == 'colour' and negative_temperature[0] == '-' and units == 'metric':
colour = 'red'
else:
colour = 'black'
return colour
"""Function to convert time objects to specified format 12/24 hours"""
"""Simple means just the hour and if 12 hours, am/pm as well"""
def to_hours(datetime_object, simple = False):
if hours == '24':
if simple == True:
converted_time = datetime_object.format('H') + '.00'
else:
converted_time = datetime_object.format('HH:mm')
else:
if simple == True:
converted_time = datetime_object.format('H a')
else:
converted_time = datetime_object.format('hh:mm')
return str(converted_time)
"""Choose font optimised for the weather section"""
fontsize = 8
font = ImageFont.truetype(NotoSans+'Medium.ttf', fontsize)
fill_height = 0.8
while font.getsize('hg')[1] <= (row_height * fill_height):
fontsize += 1
font = ImageFont.truetype(NotoSans+'.ttf', fontsize)
def main():
"""Connect to Openweathermap API and fetch weather data"""
if top_section == "Weather" and api_key != "" and owm.is_API_online() is True:
try:
clear_image('top_section')
print('Weather module: Connectivity check passed, Generating image...',
end = '')
current_weather_setup = owm.weather_at_place(location)
weather = current_weather_setup.get_weather()
"""Set-up and get weather forecast data"""
forecast = owm.three_hours_forecast(location)
"""Round the hour to the nearest multiple of 3"""
now = arrow.now(tz=get_tz())
if (now.hour % 3) != 0:
hour_gap = 3 - (now.hour % 3)
else:
hour_gap = 3
"""Prepare timings for forecasts"""
fc1 = now.replace(hours = + hour_gap)
fc2 = now.replace(hours = + hour_gap + 3)
fc3 = now.replace(hours = + hour_gap + 6)
fc4 = now.replace(hours = + hour_gap + 9)
"""Prepare forecast objects for the specified timings"""
forecast_fc1 = forecast.get_weather_at(fc1.datetime)
forecast_fc2 = forecast.get_weather_at(fc2.datetime)
forecast_fc3 = forecast.get_weather_at(fc3.datetime)
forecast_fc4 = forecast.get_weather_at(fc4.datetime)
"""Get the current temperature and forcasts temperatures"""
temperature_now = to_units(weather.get_temperature()['temp'])
temperature_fc1 = to_units(forecast_fc1.get_temperature()['temp'])
temperature_fc2 = to_units(forecast_fc2.get_temperature()['temp'])
temperature_fc3 = to_units(forecast_fc3.get_temperature()['temp'])
temperature_fc4 = to_units(forecast_fc4.get_temperature()['temp'])
"""Get current and forecast weather icon names"""
weather_icon_now = weather.get_weather_icon_name()
weather_icon_fc1 = forecast_fc1.get_weather_icon_name()
weather_icon_fc2 = forecast_fc2.get_weather_icon_name()
weather_icon_fc3 = forecast_fc3.get_weather_icon_name()
weather_icon_fc4 = forecast_fc4.get_weather_icon_name()
"""Parse current weather details"""
sunrise_time_now = arrow.get(weather.get_sunrise_time()).to(get_tz())
sunset_time_now = arrow.get(weather.get_sunset_time()).to(get_tz())
humidity_now = str(weather.get_humidity())
cloudstatus_now = str(weather.get_clouds())
weather_description_now = str(weather.get_detailed_status())
windspeed_now = weather.get_wind(unit='meters_sec')['speed']
wind_degrees = forecast_fc1.get_wind()['deg']
wind_direction = ["N","NE","E","SE","S","SW","W","NW"][round(
wind_degrees/45) % 8]
if use_beaufort == True:
wind = str([windspeed_to_beaufort.index(_) for _ in
windspeed_to_beaufort if windspeed_now < _][0])
else:
meters_sec = round(windspeed_now, ndigits = windspeed_decimal_places)
miles_per_hour = round(windspeed_now * 2,23694,
ndigits = windspeed_decimal_places)
if units == 'metric':
wind = str(meters_sec) + 'm/s'
if units == 'imperial':
wind = str(miles_per_hour) + 'mph'
if show_wind_direction == True:
wind += '({0})'.format(wind_direction)
"""Calculate the moon phase"""
def get_moon_phase():
diff = now - arrow.get(2001, 1, 1)
days = dec(diff.days) + (dec(diff.seconds) / dec(86400))
lunations = dec("0.20439731") + (days * dec("0.03386319269"))
position = lunations % dec(1)
index = math.floor((position * dec(8)) + dec("0.5"))
return {0: '\uf095',1: '\uf099',2: '\uf09c',3: '\uf0a0',
4: '\uf0a3',5: '\uf0a7',6: '\uf0aa',7: '\uf0ae' }[int(index) & 7]
moonphase = get_moon_phase()
"""Add weather details in column 1"""
write_text(coloumn_width, row_height, 'now', text_now_pos, font = font)
write_text(icon_medium, icon_medium, weathericons[weather_icon_now],
weather_icon_now_pos, font = w_font, fill_width = 0.9)
"""Add weather details in column 2"""
write_text(icon_small, icon_small, '\uf053', temperature_icon_now_pos,
font = w_font, fill_height = 0.9)
write_text(icon_small, icon_small, '\uf07a', humidity_icon_now_pos,
font = w_font, fill_height = 0.9)
if use_wind_direction_icon == False:
write_text(icon_small, icon_small, '\uf050', windspeed_icon_now_pos,
font = w_font, fill_height = 0.9)
else:
write_text(icon_small, icon_small, '\uf0b1', windspeed_icon_now_pos,
font = w_font, fill_height = 0.9, rotation = -wind_degrees)
write_text(coloumn_width-icon_small, row_height,
temperature_now, temperature_now_pos, font = font, colour =
red_temp(temperature_now))
write_text(coloumn_width-icon_small, row_height, humidity_now+'%',
humidity_now_pos, font = font)
write_text(coloumn_width-icon_small, row_height, wind,
windspeed_now_pos, font = font, autofit = True)
"""Add weather details in column 3"""
write_text(coloumn_width, row_height, moonphase , moon_phase_now_pos,
font = w_font, fill_height = 0.9)
write_text(icon_small, icon_small, '\uf051', sunrise_icon_now_pos,
font = w_font, fill_height = 0.9)
write_text(icon_small, icon_small, '\uf052', sunset_icon_now_pos,
font = w_font, fill_height = 0.9)
write_text(coloumn_width-icon_small, row_height,
to_hours(sunrise_time_now), sunrise_time_now_pos, font = font,
fill_width = 0.9)
write_text(coloumn_width-icon_small, row_height,
to_hours(sunset_time_now), sunset_time_now_pos, font = font,
fill_width = 0.9)
"""Add weather details in column 4 (forecast 1)"""
write_text(coloumn_width, row_height, to_hours(fc1, simple=True),
text_fc1_pos, font = font)
write_text(coloumn_width, row_height, weathericons[weather_icon_fc1],
icon_fc1_pos, font = w_font, fill_height = 1.0)
write_text(coloumn_width, row_height, temperature_fc1,
temperature_fc1_pos, font = font, colour = red_temp(
temperature_fc1))
"""Add weather details in column 5 (forecast 2)"""
write_text(coloumn_width, row_height, to_hours(fc2, simple=True),
text_fc2_pos, font = font)
write_text(coloumn_width, row_height, weathericons[weather_icon_fc2],
icon_fc2_pos, font = w_font, fill_height = 1.0)
write_text(coloumn_width, row_height, temperature_fc2,
temperature_fc2_pos, font = font, colour = red_temp(
temperature_fc2))
"""Add weather details in column 6 (forecast 3)"""
write_text(coloumn_width, row_height, to_hours(fc3, simple=True),
text_fc3_pos, font = font)
write_text(coloumn_width, row_height, weathericons[weather_icon_fc3],
icon_fc3_pos, font = w_font, fill_height = 1.0)
write_text(coloumn_width, row_height, temperature_fc3,
temperature_fc3_pos, font = font, colour = red_temp(
temperature_fc3))
"""Add weather details in coloumn 7 (forecast 4)"""
write_text(coloumn_width, row_height, to_hours(fc4, simple=True),
text_fc4_pos, font = font)
write_text(coloumn_width, row_height, weathericons[weather_icon_fc4],
icon_fc4_pos, font = w_font, fill_height = 1.0)
write_text(coloumn_width, row_height, temperature_fc4,
temperature_fc4_pos, font = font, colour = red_temp(
temperature_fc4))
"""Add vertical lines between forecast sections"""
draw = ImageDraw.Draw(image)
line_start_y = int(top_section_height*0.1)
line_end_y = int(top_section_height*0.9)
draw.line((coloumn4, line_start_y, coloumn4, line_end_y), fill='black')
draw.line((coloumn5, line_start_y, coloumn5, line_end_y), fill='black')
draw.line((coloumn6, line_start_y, coloumn6, line_end_y), fill='black')
draw.line((coloumn7, line_start_y, coloumn7, line_end_y), fill='black')
draw.line((0, top_section_height-border_top, top_section_width-
border_left, top_section_height-border_top),
fill='red' if display_type == 'colour' else 'black' , width=3)
weather_image = crop_image(image, 'top_section')
weather_image.save(image_path+'weather.png')
print('Done')
except Exception as e:
"""If no response was received from the openweathermap
api server, add the cloud with question mark"""
print('__________OWM-ERROR!__________')
print('Reason: ',e)
write_text(icon_medium, icon_medium, '\uf07b', weather_icon_now_pos,
font = w_font, fill_height = 1.0)
message = 'No internet connectivity or API timeout'
write_text(coloumn_width*6, row_height, message, humidity_icon_now_pos,
font = font)
weather_image = crop_image(image, 'top_section')
weather_image.save(image_path+'weather.png')
pass
if __name__ == '__main__':
main()