283 lines
8.8 KiB
Python
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)
|