Adafruit_Learning_System_Gu.../EInk_Bonnet_Event_Calendar/code.py
2022-02-23 14:08:26 -05:00

283 lines
8.8 KiB
Python

# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
from __future__ import print_function
from datetime import datetime
import time
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import textwrap
import digitalio
import busio
import board
from PIL import Image, ImageDraw, ImageFont
from adafruit_epd.epd import Adafruit_EPD
from adafruit_epd.ssd1675 import Adafruit_SSD1675
from adafruit_epd.ssd1680 import Adafruit_SSD1680
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
ecs = digitalio.DigitalInOut(board.CE0)
dc = digitalio.DigitalInOut(board.D22)
rst = digitalio.DigitalInOut(board.D27)
busy = digitalio.DigitalInOut(board.D17)
up_button = digitalio.DigitalInOut(board.D5)
up_button.switch_to_input()
down_button = digitalio.DigitalInOut(board.D6)
down_button.switch_to_input()
# If modifying these scopes, delete the file token.pickle.
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
# Check for new/deleted events every 10 seconds
QUERY_DELAY = 10 # Time in seconds to delay between querying the Google Calendar API
MAX_EVENTS_PER_CAL = 5
MAX_LINES = 2
DEBOUNCE_DELAY = 0.3
# Initialize the Display
display = Adafruit_SSD1680( # Newer eInk Bonnet
# display = Adafruit_SSD1675( # Older eInk Bonnet
122, 250, spi, cs_pin=ecs, dc_pin=dc, sramcs_pin=None, rst_pin=rst, busy_pin=busy,
)
display.rotation = 1
# RGB Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
creds = None
# The file token.pickle stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists("token.pickle"):
with open("token.pickle", "rb") as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_console()
# Save the credentials for the next run
with open("token.pickle", "wb") as token:
pickle.dump(creds, token)
service = build("calendar", "v3", credentials=creds)
current_event_id = None
last_check = None
events = []
def display_event(event_id):
event_index = search_id(event_id)
if event_index is None:
if len(events) > 0:
# Event was probably deleted while we were updating
event_index = 0
event = events[0]
else:
event = None
else:
event = events[event_index]
current_time = get_current_time()
display.fill(Adafruit_EPD.WHITE)
image = Image.new("RGB", (display.width, display.height), color=WHITE)
draw = ImageDraw.Draw(image)
event_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 24
)
time_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18
)
next_event_font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16
)
# Draw Time
current_time = get_current_time()
(font_width, font_height) = time_font.getsize(current_time)
draw.text(
(display.width - font_width - 2, 2), current_time, font=time_font, fill=BLACK,
)
if event is None:
text = "No events found"
(font_width, font_height) = event_font.getsize(text)
draw.text(
(
display.width // 2 - font_width // 2,
display.height // 2 - font_height // 2,
),
text,
font=event_font,
fill=BLACK,
)
else:
how_long = format_interval(
event["start"].get("dateTime", event["start"].get("date"))
)
draw.text(
(2, 2), how_long, font=time_font, fill=BLACK,
)
(font_width, font_height) = event_font.getsize(event["summary"])
lines = textwrap.wrap(event["summary"], width=20)
for line_index, line in enumerate(lines):
if line_index < MAX_LINES:
draw.text(
(2, line_index * font_height + 22),
line,
font=event_font,
fill=BLACK,
)
# Draw Next Event if there is one
if event_index < len(events) - 1:
next_event = events[event_index + 1]
next_time = format_event_date(
next_event["start"].get("dateTime", next_event["start"].get("date"))
)
next_item = "Then " + next_time + ": "
(font_width, font_height) = next_event_font.getsize(next_item)
draw.text(
(2, display.height - font_height * 2 - 8),
next_item,
font=next_event_font,
fill=BLACK,
)
draw.text(
(2, display.height - font_height - 2),
next_event["summary"],
font=next_event_font,
fill=BLACK,
)
display.image(image)
display.display()
def format_event_date(datestr):
event_date = datetime.fromisoformat(datestr)
# If the same day, just return time
if event_date.date() == datetime.now().date():
return event_date.strftime("%I:%M %p")
# If a future date, return date and time
return event_date.strftime("%m/%d/%y %I:%M %p")
def format_interval(datestr):
event_date = datetime.fromisoformat(datestr).replace(tzinfo=None)
delta = event_date - datetime.now()
# if < 60 minutes, return minutes
if delta.days < 0:
return "Now:"
if not delta.days and delta.seconds < 3600:
value = round(delta.seconds / 60)
return "In {} minute{}:".format(value, "s" if value > 1 else "")
# if < 24 hours return hours
if not delta.days:
value = round(delta.seconds / 3600)
return "In {} hour{}:".format(value, "s" if value > 1 else "")
return "In {} day{}:".format(delta.days, "s" if delta.days > 1 else "")
def search_id(event_id):
if event_id is not None:
for index, event in enumerate(events):
if event["id"] == event_id:
return index
return None
def get_current_time():
now = datetime.now()
return now.strftime("%I:%M %p")
current_time = get_current_time()
def get_events(calendar_id):
print("Fetching Events for {}".format(calendar_id))
page_token = None
events = (
service.events()
.list(
calendarId=calendar_id,
timeMin=now,
maxResults=MAX_EVENTS_PER_CAL,
singleEvents=True,
orderBy="startTime",
)
.execute()
)
return events.get("items", [])
def get_all_calendar_ids():
page_token = None
calendar_ids = []
while True:
print("Fetching Calendar IDs")
calendar_list = service.calendarList().list(pageToken=page_token).execute()
for calendar_list_entry in calendar_list["items"]:
calendar_ids.append(calendar_list_entry["id"])
page_token = calendar_list.get("nextPageToken")
if not page_token:
break
return calendar_ids
while True:
last_event_id = current_event_id
last_time = current_time
if last_check is None or time.monotonic() >= last_check + QUERY_DELAY:
# Call the Calendar API
now = datetime.utcnow().isoformat() + "Z"
calendar_ids = get_all_calendar_ids()
events = []
for calendar_id in calendar_ids:
events += get_events(calendar_id)
# Sort Events by Start Time
events = sorted(
events, key=lambda k: k["start"].get("dateTime", k["start"].get("date"))
)
last_check = time.monotonic()
# Update the current time
current_time = get_current_time()
if not events:
current_event_id = None
current_index = None
else:
if current_event_id is None:
current_index = 0
else:
current_index = search_id(current_event_id)
if current_index is not None:
# Check for Button Presses
if up_button.value != down_button.value:
if not up_button.value and current_index < len(events) - 1:
current_index += 1
time.sleep(DEBOUNCE_DELAY)
if not down_button.value and current_index > 0:
current_index -= 1
time.sleep(DEBOUNCE_DELAY)
current_event_id = events[current_index]["id"]
else:
current_event_id = None
if current_event_id != last_event_id or current_time != last_time:
display_event(current_event_id)