This repository has been archived by the owner on Jun 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
/
remind.py
478 lines (423 loc) · 19.7 KB
/
remind.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
#!/usr/bin/python
'''*****************************************************************************************************************
Pi Remind (HD)
By John M. Wargo
www.johnwargo.com
This application connects to a Google Calendar and determines whether there are any appointments in the next
few minutes and flashes some LEDs if there are. The project uses a Raspberry Pi 2 device with a Pimoroni
Unicorn HAT HD (a 16x16 matrix of bright, multi-colored LEDs) to display an obnoxious reminder every minute,
changing color at 10 minutes (WHITE), 5 minutes (YELLOW) and 2 minutes (multi-color swirl).
Google Calendar example code: https://developers.google.com/google-apps/calendar/quickstart/python
********************************************************************************************************************'''
# todo: Add configurable option for ignoring tentative appointments
from __future__ import print_function
import datetime
import math
import os
import socket
import sys
import time
import pytz
import unicornhathd
from dateutil import parser
from googleapiclient.discovery import build
from httplib2 import Http
from oauth2client import client, file, tools
try:
import argparse
flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()
except ImportError:
flags = None
# =============================================================================
# Borrowed from: https://github.com/pimoroni/unicorn-hat-hd/blob/master/examples/text.py
# =============================================================================
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
exit("This script requires the pillow module\nInstall with: sudo pip install pillow")
# Use `fc-list` to show a list of installed fonts on your system,
# or `ls /usr/share/fonts/` and explore.
FONT = ("/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", 12)
# =============================================================================
HASH = '#'
HASHES = '#############################################'
# Event search scope (searches this many minutes in the future for events). Increase this value to get reminders
# earlier. The app displays WHITE lights from this limit up to FIRST_THRESHOLD
SEARCH_LIMIT = 10 # minutes
# Reminder thresholds
FIRST_THRESHOLD = 5 # minutes, WHITE lights before this
# RED for anything less than (and including) the second threshold
SECOND_THRESHOLD = 2 # minutes, YELLOW lights before this
# Reboot Options - Added this to enable users to reboot the pi after a certain number of failed retries.
# I noticed that on power loss, the Pi looses connection to the network and takes a reboot after the network
# comes back to fix it.
REBOOT_COUNTER_ENABLED = False
REBOOT_NUM_RETRIES = 10
reboot_counter = 0 # counter variable, tracks retry events.
# COLORS
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
ORANGE = (255, 153, 0)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
BLACK = (0, 0, 0)
# constants used in the app to display status
CHECKING_COLOR = BLUE
SUCCESS_COLOR = GREEN
FAILURE_COLOR = RED
# JMW Added 20170414 to fix an issue when there's an error connecting to the
# Google Calendar API. The app needs to track whether there's an existing
# error through the process. If there is, then when checking again for entries
# the app will leave the light red while checking. Setting it to green if
# successful.
has_error = False
def display_text(message, color=WHITE):
# do we have a message?
if len(message) > 0:
# then display it
# code borrowed from: https://github.com/pimoroni/unicorn-hat-hd/blob/master/examples/text.py
text_x = u_width
text_y = 2
font_file, font_size = FONT
font = ImageFont.truetype(font_file, font_size)
# =====================================================================
# I'm really not sure what all this code does...that's what happens when you 'borrow' code
# it basically sets up the width of the display string to include the string as well as enough
# space to scroll it off the screen
# =====================================================================
text_width, text_height = u_width, 0
w, h = font.getsize(message)
text_width += w + u_width
text_height = max(text_height, h)
text_width += u_width + text_x + 1
image = Image.new("RGB", (text_width, max(16, text_height)), (0, 0, 0))
draw = ImageDraw.Draw(image)
offset_left = 0
draw.text((text_x + offset_left, text_y), message, color, font=font)
offset_left += font.getsize(message)[0] + u_width
for scroll in range(text_width - u_width):
for x in range(u_width):
for y in range(u_height):
pixel = image.getpixel((x + scroll, y))
r, g, b = [int(n) for n in pixel]
unicornhathd.set_pixel(u_width - 1 - x, y, r, g, b)
unicornhathd.show()
time.sleep(0.01)
unicornhathd.off()
# =====================================================================
def swirl(x, y, step):
# modified from: https://github.com/pimoroni/unicorn-hat-hd/blob/master/examples/demo.py
x -= (u_width / 2)
y -= (u_height / 2)
dist = math.sqrt(pow(x, 2) + pow(y, 2)) / 2.0
angle = (step / 10.0) + (dist * 1.5)
s = math.sin(angle)
c = math.cos(angle)
xs = x * c - y * s
ys = x * s + y * c
r = abs(xs + ys)
r = r * 12.0
r -= 20
return r, r + (s * 130), r + (c * 130)
def do_swirl(duration):
# modified from: https://github.com/pimoroni/unicorn-hat-hd/blob/master/examples/demo.py
step = 0
for i in range(duration):
for y in range(u_height):
for x in range(u_width):
r, g, b = swirl(x, y, step)
r = int(max(0, min(255, r)))
g = int(max(0, min(255, g)))
b = int(max(0, min(255, b)))
unicornhathd.set_pixel(x, y, r, g, b)
step += 2
unicornhathd.show()
time.sleep(0.01)
# turn off all lights when you're done
unicornhathd.off()
def set_activity_light(color, increment):
# used to turn on one LED at a time across the bottom row of lights. The app uses this as an unobtrusive
# indicator when it connects to Google to check the calendar. Its intended as a subtle reminder that things
# are still working.
global current_activity_light
# turn off (clear) any lights that are on
unicornhathd.off()
if increment:
# OK. Which light will we be illuminating?
if current_activity_light < 1:
# start over at the beginning when you're at the end of the row
current_activity_light = u_width
# increment the current light (to the next one)
current_activity_light -= 1
# set the pixel color
unicornhathd.set_pixel(current_activity_light, indicator_row, *color)
# show the pixel
unicornhathd.show()
def set_all(color):
unicornhathd.set_all(*color)
unicornhathd.show()
def flash_all(flash_count, delay, color):
# light all of the LEDs in a RGB single color. Repeat 'flash_count' times
# keep illuminated for 'delay' value
for index in range(flash_count):
# fill the light buffer with the specified color
unicornhathd.set_all(*color)
# show the color
unicornhathd.show()
# wait a bit
time.sleep(delay)
# turn everything off
unicornhathd.off()
# wait a bit more
time.sleep(delay)
def flash_random(flash_count, delay, between_delay=0):
# Copied from https://github.com/pimoroni/unicorn-hat-hd/blob/master/examples/test.py
for index in range(flash_count):
# fill the light buffer with random colors
unicornhathd._buf = unicornhathd.numpy.random.randint(low=0, high=255, size=(16, 16, 3))
# show the colors
unicornhathd.show()
# wait a bit
time.sleep(delay)
# turn everything off
unicornhathd.off()
# do we have a between_delay value??
if between_delay > 0:
# wait a bit more
time.sleep(between_delay)
def has_reminder(event):
# Return true if there's a reminder set for the event
# First, check to see if there is a default reminder set
# Yes, I know I could have done this and the next check without using variables
# this approach just makes the code easier to understand
has_default_reminder = event['reminders'].get('useDefault')
if has_default_reminder:
# if yes, then we're good
return True
else:
# are there overrides set for reminders?
overrides = event['reminders'].get('overrides')
if overrides:
# OK, then we have a reminder to use
return True
# if we got this far, then there must not be a reminder set
return False
def get_next_event():
global has_error, reboot_counter
# modified from https://developers.google.com/google-apps/calendar/quickstart/python
# get all of the events on the calendar from now through 10 minutes from now
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'Getting next event')
# this 'now' is in a different format (UTC)
now = datetime.datetime.utcnow()
# Calculate a time search_limit from now
then = now + datetime.timedelta(minutes=SEARCH_LIMIT)
# if we don't have an error from the previous attempt, then change the LED color
# otherwise leave it alone (it should already be red, so it will stay that way).
if not has_error:
# turn on a sequential CHECKING_COLOR LED to show that you're requesting data from the Google Calendar API
set_activity_light(CHECKING_COLOR, True)
try:
# ask Google for the calendar entries
events_result = service.events().list(
# get all of them between now and 10 minutes from now
calendarId='primary',
timeMin=now.isoformat() + 'Z',
timeMax=then.isoformat() + 'Z',
singleEvents=True,
orderBy='startTime').execute()
# turn on the SUCCESS_COLOR LED so you'll know data was returned from the Google calendar API
set_activity_light(SUCCESS_COLOR, False)
# Get the event list
event_list = events_result.get('items', [])
# initialize this here, setting it to true later if we encounter an error
has_error = False
# reset the reboot counter, since everything worked so far
reboot_counter = 0
# did we get a return value?
if not event_list:
# no? Then no upcoming events at all, so nothing to do right now
print(datetime.datetime.now(), 'No entries returned')
return None
else:
# what time is it now?
current_time = pytz.utc.localize(datetime.datetime.utcnow())
# loop through the events in the list
for event in event_list:
# we only care about events that have a start time
start = event['start'].get('dateTime')
# return the first event that has a start time
# so, first, do we have a start time for this event?
if start:
# When does the appointment start?
# Convert the string it into a Python dateTime object so we can do math on it
event_start = parser.parse(start)
# does the event start in the future?
if current_time < event_start:
# only use events that have a reminder set
if has_reminder(event):
# no? So we can use it
event_summary = event['summary'] if 'summary' in event else 'No Title'
print('Found event:', event_summary)
print('Event starts:', start)
# figure out how soon it starts
time_delta = event_start - current_time
# Round to the nearest minute and return with the object
event['num_minutes'] = time_delta.total_seconds() // 60
return event
except Exception as e:
# Something went wrong, tell the user (just in case they have a monitor on the Pi)
print('\nException type:', type(e))
# not much else we can do here except to skip this attempt and try again later
print('Error:', sys.exc_info()[0])
# light up the array with FAILURE_COLOR LEDs to indicate a problem
flash_all(1, 2, FAILURE_COLOR)
# now set the current_activity_light to FAILURE_COLOR to indicate an error state
# with the last reading
set_activity_light(FAILURE_COLOR, False)
# we have an error, so make note of it
has_error = True
# check to see if reboot is enabled
if REBOOT_COUNTER_ENABLED:
# increment the counter
reboot_counter += 1
print('Incrementing the reboot counter ({})'.format(reboot_counter))
# did we reach the reboot threshold?
if reboot_counter == REBOOT_NUM_RETRIES:
# Reboot the Pi
for i in range(1, 10):
print('Rebooting in {} seconds'.format(i))
time.sleep(1)
os.system("sudo reboot")
# if we got this far and haven't returned anything, then there's no appointments in the specified time
# range, or we had an error, so...
return None
def calendar_loop():
# initialize the lastMinute variable to the current time to start
last_minute = datetime.datetime.now().minute
# on startup, just use the previous minute as lastMinute, that way the app
# will check for appointments immediately on startup.
if last_minute == 0:
last_minute = 59
else:
last_minute -= 1
# infinite loop to continuously check Google Calendar for future entries
while 1:
# get the current minute
current_minute = datetime.datetime.now().minute
# is it the same minute as the last time we checked?
if current_minute != last_minute:
# reset last_minute to the current_minute, of course
last_minute = current_minute
# we've moved a minute, so we have work to do
# get the next calendar event (within the specified time limit [in minutes])
next_event = get_next_event()
# do we get an event?
if next_event is not None:
num_minutes = next_event['num_minutes']
if num_minutes != 1:
print('Starts in {} minutes\n'.format(num_minutes))
else:
print('Starts in 1.0 minute\n')
# is the appointment between 10 and 5 minutes from now?
if num_minutes >= FIRST_THRESHOLD:
# Flash the lights in WHITE
flash_all(1, 0.25, WHITE)
# display the event summary
display_text(next_event['summary'], WHITE)
# set the activity light to WHITE as an indicator
set_activity_light(WHITE, False)
# is the appointment less than 5 minutes but more than 2 minutes from now?
elif num_minutes > SECOND_THRESHOLD:
# Flash the lights YELLOW
flash_all(2, 0.25, YELLOW)
# display the event summary
display_text(next_event['summary'], YELLOW)
# set the activity light to YELLOw as an indicator
set_activity_light(YELLOW, False)
else:
# hmmm, less than 2 minutes, almost time to start!
# swirl the lights. Longer every second closer to start time
do_swirl(int((4 - num_minutes) * 50))
# display the event summary
display_text(next_event['summary'], ORANGE)
# set the activity light to SUCCESS_COLOR (green by default)
set_activity_light(ORANGE, False)
# wait a second then check again
# You can always increase the sleep value below to check less often
time.sleep(1)
def main():
# used to setup our status indicator at the bottom of the led array
global current_activity_light, indicator_row, service, u_height, u_width
# tell the user what we're doing...
print('\n')
print(HASHES)
print(HASH, 'Pi Remind (HD) ', HASH)
print(HASH, 'https://github.com/johnwargo/pi-remind-hd', HASH)
print(HASH, 'By John M. Wargo (www.johnwargo.com) ', HASH)
print(HASHES)
# Clear the display (just in case)
unicornhathd.clear()
# Initialize all LEDs to black
unicornhathd.set_all(0, 0, 0)
# set the display orientation to zero degrees
unicornhathd.rotation(90)
# set u_width and u_height with the appropriate parameters for the HAT
u_width, u_height = unicornhathd.get_shape()
# calculate where we want to put the indicator light
indicator_row = u_height - 1
# The app flashes a GREEN light in the first row every time it connects to Google to check the calendar.
# The LED increments every time until it gets to the other side then starts over at the beginning again.
# The current_activity_light variable keeps track of which light lit last. At start it's at -1 and goes from there.
current_activity_light = u_width
# Set a specific brightness level for the Pimoroni Unicorn HAT, otherwise it's pretty bright.
# Comment out the line below to see what the default looks like.
unicornhathd.brightness(0.5)
# output whether reboot mode is enabled
if REBOOT_COUNTER_ENABLED:
print('Reboot enabled ({} retries)'.format(REBOOT_NUM_RETRIES))
try:
# Initialize the Google Calendar API stuff
print('Initializing the Google Calendar API')
# Google says: If modifying these scopes, delete your previously saved credentials at ~/.credentials/client_secret.json
# On the pi, it's in /root/.credentials/
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
store = file.Storage('google_api_token.json')
creds = store.get()
if not creds or creds.invalid:
flow = client.flow_from_clientsecrets('client_secret.json', SCOPES)
creds = tools.run_flow(flow, store)
service = build('calendar', 'v3', http=creds.authorize(Http()))
# Set the timeout for the rest of the Google API calls.
# need this at its default (infinity, i think) during the registration process.
socket.setdefaulttimeout(10) # 10 seconds
except Exception as e:
print('\nException type:', type(e))
# not much else we can do here except to skip this attempt and try again later
print('Error:', sys.exc_info()[0])
print('Unable to initialize Google Calendar API')
# make all the LEDs red
set_all(FAILURE_COLOR)
# Wait 5 seconds (so the user can see the error color on the display)
time.sleep(5)
# turn off all of the LEDs
unicornhathd.off()
# then exit, nothing else we can do here, right?
sys.exit(0)
print('Application initialized\n')
# flash some random LEDs just for fun...
flash_random(5, 0.5)
# blink all the LEDs GREEN to let the user know the hardware is working
flash_all(3, 0.10, GREEN)
calendar_loop()
if __name__ == '__main__':
try:
# do our stuff
main()
except KeyboardInterrupt:
# turn off all of the LEDs
unicornhathd.off()
# tell the user we're exiting
print('\nExiting application\n')
# exit the application
sys.exit(0)