Skip to content

Commit cc13cbc

Browse files
author
brentru
committed
add magtag gcal
1 parent 5b776cf commit cc13cbc

File tree

5 files changed

+360
-0
lines changed

5 files changed

+360
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Google OAuth2.0 Workflow for MagTag
2+
import ssl
3+
import board
4+
import wifi
5+
import socketpool
6+
import adafruit_requests as requests
7+
from adafruit_oauth2 import OAuth2
8+
from adafruit_display_text.label import Label
9+
from adafruit_bitmap_font import bitmap_font
10+
from adafruit_magtag.magtag import Graphics
11+
from adafruit_display_shapes.rect import Rect
12+
13+
# Add a secrets.py to your filesystem that has a dictionary called secrets with "ssid" and
14+
# "password" keys with your WiFi credentials. DO NOT share that file or commit it into Git or other
15+
# source control.
16+
# pylint: disable=no-name-in-module,wrong-import-order
17+
try:
18+
from secrets import secrets
19+
except ImportError:
20+
print("Credentials and tokens are kept in secrets.py, please add them there!")
21+
raise
22+
23+
print("Connecting to %s" % secrets["ssid"])
24+
wifi.radio.connect(secrets["ssid"], secrets["password"])
25+
print("Connected to %s!" % secrets["ssid"])
26+
27+
pool = socketpool.SocketPool(wifi.radio)
28+
requests = requests.Session(pool, ssl.create_default_context())
29+
30+
font_small = bitmap_font.load_font("/fonts/Arial-12.pcf")
31+
font_large = bitmap_font.load_font("/fonts/Arial-14.pcf")
32+
33+
graphics = Graphics(auto_refresh=False)
34+
display = graphics.display
35+
36+
background = Rect(0, 0, 296, 128, fill=0xFFFFFF)
37+
graphics.splash.append(background)
38+
39+
label_overview_text = Label(
40+
font_large,
41+
x=0,
42+
y=10,
43+
line_spacing=0.75,
44+
color=0x000000,
45+
text="Authorize this device with Google:",
46+
)
47+
graphics.splash.append(label_overview_text)
48+
49+
label_verification_url = Label(
50+
font_small, x=0, y=40, line_spacing=0.75, color=0x000000, max_glyphs=90
51+
)
52+
graphics.splash.append(label_verification_url)
53+
54+
label_user_code = Label(
55+
font_small, x=0, y=80, color=0x000000, line_spacing=0.75, max_glyphs=50
56+
)
57+
graphics.splash.append(label_user_code)
58+
59+
label_qr_code = Label(
60+
font_small, x=0, y=100, color=0x000000, text="Or scan the QR code:"
61+
)
62+
graphics.splash.append(label_qr_code)
63+
64+
# Set scope(s) of access required by the API you're using
65+
scopes = ["https://www.googleapis.com/auth/calendar.readonly"]
66+
67+
# Initialize an OAuth2 object
68+
google_auth = OAuth2(
69+
requests, secrets["google_client_id"], secrets["google_client_secret"], scopes
70+
)
71+
72+
# Request device and user codes
73+
# https://developers.google.com/identity/protocols/oauth2/limited-input-device#step-1:-request-device-and-user-codes
74+
google_auth.request_codes()
75+
76+
# Display user code and verification url
77+
# NOTE: If you are displaying this on a screen, ensure the text label fields are
78+
# long enough to handle the user_code and verification_url.
79+
# Details in link below:
80+
# https://developers.google.com/identity/protocols/oauth2/limited-input-device#displayingthecode
81+
print(
82+
"1) Navigate to the following URL in a web browser:", google_auth.verification_url
83+
)
84+
print("2) Enter the following code:", google_auth.user_code)
85+
label_verification_url.text = (
86+
"1. On your computer or mobile device,\ngo to %s" % google_auth.verification_url
87+
)
88+
label_user_code.text = "2. Enter code: %s" % google_auth.user_code
89+
90+
graphics.qrcode(google_auth.verification_url.encode(), qr_size=2, x=240, y=70)
91+
board.DISPLAY.show(graphics.splash)
92+
display.refresh()
93+
94+
# Poll Google's authorization server
95+
print("Waiting for browser authorization...")
96+
if not google_auth.wait_for_authorization():
97+
raise RuntimeError("Timed out waiting for browser response!")
98+
99+
print("Successfully Authenticated with Google!")
100+
print("Add the following lines to your secrets.py file:")
101+
print("\t'google_access_token' " + ":" + " '%s'," % google_auth.access_token)
102+
print("\t'google_refresh_token' " + ":" + " '%s'" % google_auth.refresh_token)
103+
104+
graphics.splash.pop()
105+
graphics.splash.pop()
106+
graphics.splash.pop()
107+
108+
label_overview_text.text = "Successfully Authenticated!"
109+
label_verification_url.text = (
110+
"Check the REPL for tokens to add\n\tto your secrets.py file"
111+
)
112+
display.refresh()

MagTag_Google_Calendar/code.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# MagTag Google Calendar Event Viewer
2+
# Brent Rubell for Adafruit Industries, 2021
3+
import time
4+
import ssl
5+
import board
6+
import rtc
7+
import wifi
8+
import socketpool
9+
import adafruit_requests as requests
10+
from adafruit_oauth2 import OAuth2
11+
from adafruit_display_shapes.line import Line
12+
from adafruit_bitmap_font import bitmap_font
13+
from adafruit_display_text import label
14+
from adafruit_magtag.magtag import MagTag
15+
16+
# Calendar ID
17+
CALENDAR_ID = "[email protected]"
18+
19+
# Maximum amount of events to display
20+
MAX_EVENTS = 3
21+
22+
# Amount of time to wait between refreshing the calendar, in minutes
23+
REFRESH_TIME = 15
24+
25+
# Dict. of month names for pretty-printing the header
26+
MONTHS = {
27+
1: "Jan",
28+
2: "Feb",
29+
3: "Mar",
30+
4: "Apr",
31+
5: "May",
32+
6: "Jun",
33+
7: "Jul",
34+
8: "Aug",
35+
9: "Sep",
36+
10: "Oct",
37+
11: "Nov",
38+
12: "Dec",
39+
}
40+
41+
# Add a secrets.py to your filesystem that has a dictionary called secrets with "ssid" and
42+
# "password" keys with your WiFi credentials. DO NOT share that file or commit it into Git or other
43+
# source control.
44+
# pylint: disable=no-name-in-module,wrong-import-order
45+
try:
46+
from secrets import secrets
47+
except ImportError:
48+
print("Credentials and tokens are kept in secrets.py, please add them there!")
49+
raise
50+
51+
print("Connecting to %s" % secrets["ssid"])
52+
wifi.radio.connect(secrets["ssid"], secrets["password"])
53+
print("Connected to %s!" % secrets["ssid"])
54+
55+
pool = socketpool.SocketPool(wifi.radio)
56+
requests = requests.Session(pool, ssl.create_default_context())
57+
58+
magtag = MagTag()
59+
r = rtc.RTC()
60+
61+
# Initialize an OAuth2 object with GCal API scope
62+
scopes = ["https://www.googleapis.com/auth/calendar.readonly"]
63+
google_auth = OAuth2(
64+
requests,
65+
secrets["google_client_id"],
66+
secrets["google_client_secret"],
67+
scopes,
68+
secrets["google_access_token"],
69+
secrets["google_refresh_token"],
70+
)
71+
72+
if not google_auth.refresh_access_token():
73+
raise RuntimeError("Unable to refresh access token - has the token been revoked?")
74+
75+
def get_current_time(time_max=False):
76+
"""Gets local time from Adafruit IO and converts to RFC3339 timestamp."""
77+
# Get local time from Adafruit IO
78+
magtag.get_local_time(secrets["timezone"])
79+
# Format as RFC339 timestamp
80+
cur_time = r.datetime
81+
if time_max: # maximum time to fetch events is midnight (4:59:59UTC)
82+
cur_time_max = time.struct_time(
83+
cur_time[0], cur_time[1], cur_time[2] + 1, 4, 59, 59, 0, -1, -1
84+
)
85+
cur_time = cur_time_max
86+
cur_time = "{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}{:s}".format(
87+
cur_time[0],
88+
cur_time[1],
89+
cur_time[2],
90+
cur_time[3],
91+
cur_time[4],
92+
cur_time[5],
93+
"Z",
94+
)
95+
return cur_time
96+
97+
98+
def get_calendar_events(calendar_id, max_events, time_min):
99+
"""Returns events on a specified calendar.
100+
Response is a list of events ordered by their start date/time in ascending order.
101+
"""
102+
time_max = get_current_time(time_max=True)
103+
print("Fetching calendar events from {0} to {1}".format(time_min, time_max))
104+
105+
headers = {
106+
"Authorization": "Bearer " + google_auth.access_token,
107+
"Accept": "application/json",
108+
"Content-Length": "0",
109+
}
110+
url = (
111+
"https://www.googleapis.com/calendar/v3/calendars/{0}"
112+
"/events?maxResults={1}&timeMin={2}&timeMax={3}&orderBy=startTime"
113+
"&singleEvents=true".format(calendar_id, max_events, time_min, time_max)
114+
)
115+
resp = requests.get(url, headers=headers)
116+
resp_json = resp.json()
117+
if "error" in resp_json:
118+
raise RuntimeError("Error:", resp_json)
119+
resp.close()
120+
# parse the 'items' array so we can iterate over it easier
121+
items = []
122+
resp_items = resp_json["items"]
123+
if not resp_items:
124+
print("No events scheduled for today!")
125+
for event in range(0, len(resp_items)):
126+
items.append(resp_items[event])
127+
return items
128+
129+
130+
def format_datetime(datetime, pretty_date=False):
131+
"""Formats ISO-formatted datetime returned by Google Calendar API into
132+
a struct_time.
133+
:param str datetime: Datetime string returned by Google Calendar API
134+
:return: struct_time
135+
136+
"""
137+
times = datetime.split("T")
138+
the_date = times[0]
139+
the_time = times[1]
140+
year, month, mday = [int(x) for x in the_date.split("-")]
141+
the_time = the_time.split("-")[0]
142+
if "Z" in the_time:
143+
the_time = the_time.split("Z")[0]
144+
hours, minutes, _ = [int(x) for x in the_time.split(":")]
145+
am_pm = "am"
146+
if hours >= 12:
147+
am_pm = "pm"
148+
# convert to 12hr time
149+
if not hours == 12:
150+
hours -= 12
151+
# via https://github.com/micropython/micropython/issues/3087
152+
formatted_time = "{:02d}:{:02d}{:s}".format(hours, minutes, am_pm)
153+
if pretty_date: # return a nice date for header label
154+
formatted_date = "{}. {:02d}, {:04d} ".format(MONTHS[month], mday, year)
155+
return formatted_date
156+
# Event occurs today, return the time only
157+
return formatted_time
158+
159+
160+
def display_calendar_events(resp_events):
161+
# Display all calendar events
162+
for event_idx in range(len(resp_events)):
163+
event = resp_events[event_idx]
164+
# wrap event name around second line if necessary
165+
event_name = magtag.wrap_nicely(event["summary"], 25)
166+
event_name = "\n".join(event_name[0:2]) # only wrap 2 lines, truncate third..
167+
event_start = event["start"]["dateTime"]
168+
print("-" * 40)
169+
print("Event Description: ", event_name)
170+
print("Event Time:", format_datetime(event_start))
171+
print("-" * 40)
172+
# Generate labels holding event info
173+
label_event_time = label.Label(
174+
font_event,
175+
x=7,
176+
y=35 + (event_idx * 35),
177+
color=0x000000,
178+
text=format_datetime(event_start),
179+
)
180+
magtag.splash.append(label_event_time)
181+
182+
label_event_desc = label.Label(
183+
font_event,
184+
x=88,
185+
y=35 + (event_idx * 35),
186+
color=0x000000,
187+
text=event_name,
188+
line_spacing=0.65,
189+
)
190+
magtag.splash.append(label_event_desc)
191+
192+
# DisplayIO Setup
193+
magtag.set_background(0xFFFFFF)
194+
195+
# Add the header
196+
line_header = Line(0, 25, 320, 25, color=0x000000)
197+
magtag.splash.append(line_header)
198+
199+
font_h1 = bitmap_font.load_font("fonts/Arial-Bold-18.pcf")
200+
font_h1.load_glyphs(
201+
b"abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-,. "
202+
)
203+
label_header = label.Label(
204+
font_h1, x=(board.DISPLAY.width // 5) + 1, y=10, color=0x000000, max_glyphs=13
205+
)
206+
magtag.splash.append(label_header)
207+
208+
# Set up calendar event fonts
209+
font_event = bitmap_font.load_font("fonts/Arial-12.pcf")
210+
font_event.load_glyphs(
211+
b"abcdefghjiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-:/ ()"
212+
)
213+
214+
if not google_auth.refresh_access_token():
215+
raise RuntimeError("Unable to refresh access token - has the token been revoked?")
216+
access_token_obtained = int(time.monotonic())
217+
218+
while True:
219+
# check if we need to refresh token
220+
if (
221+
int(time.monotonic()) - access_token_obtained
222+
>= google_auth.access_token_expiration
223+
):
224+
print("Access token expired, refreshing...")
225+
if not google_auth.refresh_access_token():
226+
raise RuntimeError(
227+
"Unable to refresh access token - has the token been revoked?"
228+
)
229+
access_token_obtained = int(time.monotonic())
230+
231+
# fetch calendar events!
232+
print("fetching local time...")
233+
now = get_current_time()
234+
235+
# setup header label
236+
label_header.text = format_datetime(now, pretty_date=True)
237+
238+
print("fetching calendar events...")
239+
events = get_calendar_events(CALENDAR_ID, MAX_EVENTS, now)
240+
241+
print("displaying events")
242+
display_calendar_events(events)
243+
244+
board.DISPLAY.show(magtag.splash)
245+
board.DISPLAY.refresh()
246+
247+
print("Sleeping for %d minutes" % REFRESH_TIME)
248+
magtag.exit_and_deep_sleep(REFRESH_TIME * 60)
218 KB
Binary file not shown.
228 KB
Binary file not shown.
248 KB
Binary file not shown.

0 commit comments

Comments
 (0)