Gmail total unread messages display + local weather info – Raspberry Pi Zero + OLED.

Continuing from the previous post, this addition adds both the current weather info + forecast weather info for this time tomorrow along with the total Gmail unread email count to display on the OLED using a Raspberry Pi Zero.

In the following post I will retrace my steps as I:

1) Sign up for an API key at openweathermap.com.

We’ll be using OpenWeatherMap to gather weather data. Navigate to their sign up page: https://home.openweathermap.org/users/sign_up.

Follow their process to get your API key. Once you have it we can move to our “quickstart.py” that we created in the previous post.

2) On your Pi, install the “pyowm” pip package & use your API key to grab relevant weather data to display on the OLED.

We’ll be using the “pyowm” python wrapper for the OpenWeatherMap API.

Install the pyowm pip package:

% pip3 install pyowm

Import the pyowm library in the “quickstart.py” file we made previously:

# Helper "wrapper" for the OpenWeatherMaps API
import pyowm

Next, we’ll use the necessary logic to retrieve the local weather info. We’ll eventually need to get the current temperature, the temperature for this time tomorrow, the current weather conditions, and finally the weather conditions for this time tomorrow.

Let’s first add some more “editable vars” to the top of quickstart.py:

OPEN_WEATHER_MAP_API_KEY = "<api-key>"
OPEN_WEATHER_MAP_LOCATION = "san diego,us" # city,country code
OPEN_WEATHER_MAP_TEMP_SCALE = "fahrenheit" # fahrenheit or celsius
### END Editable Vars

If you’re following along from the previous post, update some font values for the OLED display in “quickstart.py”:

# Load default font.
#font = ImageFont.load_default()
font = ImageFont.truetype("fonts/Arial.ttf", 16)
font_email = ImageFont.truetype("fonts/Arial.ttf", 11)
font_sky = ImageFont.truetype("fonts/Arial.ttf", 12)

And now let’s add the logic to connect to the OpenWeatherMap API, grab the relevant weather info, and prepare it to display on the OLED:

# Create a new instance of the pyowm wrapper with your API key.
owm = pyowm.OWM(OPEN_WEATHER_MAP_API_KEY)  
# Search for current weather in OPEN_WEATHER_MAP_LOCATION
observation = owm.weather_at_place(OPEN_WEATHER_MAP_LOCATION)
w = observation.get_weather()
# Get the current temperature.
temperature = round(w.get_temperature(OPEN_WEATHER_MAP_TEMP_SCALE)['temp'])
# Get the current weather info
sky = w.get_detailed_status()
# Create two variables that will store tomorrows values.
tomorrows_sky = ''
tomorrows_temp = ''

# Create a new datetime & add 1 day. 
current_time = datetime.datetime.now()
current_time_adjusted = current_time + datetime.timedelta(days=+1)

# Get the forecast for the next few days in 3 hour time blocks.
fc = owm.three_hours_forecast(OPEN_WEATHER_MAP_LOCATION)
f = fc.get_forecast()
# Loop through the forecast list.
for weather in f:
    # Get the time info for the current element in the weather forecast list.
    ref_time = weather.get_reference_time('date').replace(tzinfo=None)
    # Next, find the closest time that matches this time tomorrow 
    compare = current_time_adjusted > ref_time
    if(compare is False):
        tomorrows_sky = weather.get_detailed_status()
        tomorrows_temp = round(weather.get_temperature(OPEN_WEATHER_MAP_TEMP_SCALE)['temp'])
        print(weather.get_temperature(OPEN_WEATHER_MAP_TEMP_SCALE))
        print("tomorrows_sky", tomorrows_sky)
        print("tomorrows_temp", tomorrows_temp)
        print("weather.get_reference_time('iso')", weather.get_reference_time('iso'))
        break

print("temperature:", temperature)
print("sky:", sky)

And now we can add/update the code to display it on the OLED:

    draw.text((x, top),       str(email_count)+" "+text_modified,  font=font, fill=255)
	draw.text((x, top+19),    str(temperature)+"°"+" / "+str(tomorrows_temp)+"°",  font=font, fill=255)
	draw.text((x, top+38),    str(sky.title() + " / " + tomorrows_sky.title()),  font=font_sky, fill=255)
	draw.text((x, top+54),    "IP: " + str(IP),  font=font_email, fill=255)

The full and final “quickstart.py” with all the updated code:

from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

# Adafruits OLED libraries + others.
import Adafruit_GPIO.SPI as SPI
import Adafruit_SSD1306

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

# Library used to run applications in a python program.
import subprocess

# Regex for scrubbing the IP of extra characters.
import re

# Library used For our delay timer.
import time

# Library used for calculating screensaver logic.
import datetime

# Helper "wrapper" for the OpenWeatherMaps API
import pyowm

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
TEXT_TO_APPEND_TO_COUNT = "Unread Emails"
DELAY_TIME_TO_QUERY_API = 10 # seconds

SCREENSAVER_TIME_BEGIN = "00:00:00" #24h - Midnight
SCREENSAVER_TIME_END = "08:00:00" 

OPEN_WEATHER_MAP_API_KEY = "<api-key>"
OPEN_WEATHER_MAP_LOCATION = "san diego,us" # city,country code
OPEN_WEATHER_MAP_TEMP_SCALE = "fahrenheit" # fahrenheit or celsius
### END Editable Vars

# Raspberry Pi pin configuration: (taken from Adafruits stats.py)
RST = None     # on the PiOLED this pin isnt used

# 128x64 display with hardware I2C: (taken from Adafruits stats.py)
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST) 

# Initialize library. (taken from Adafruits stats.py)
disp.begin()

# Clear display. (taken from Adafruits stats.py)
disp.clear()
disp.display()

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color. 
# (taken from Adafruits stats.py)
width = disp.width
height = disp.height
image = Image.new('1', (width, height))

# Get drawing object to draw on image. 
# (taken from Adafruits stats.py)
draw = ImageDraw.Draw(image)

# Draw a black filled box to clear the image. 
# (taken from Adafruits stats.py)
draw.rectangle((0,0,width,height), outline=0, fill=0)

# Load default font.
#font = ImageFont.load_default()
font = ImageFont.truetype("fonts/Arial.ttf", 16)
font_email = ImageFont.truetype("fonts/Arial.ttf", 11)
font_sky = ImageFont.truetype("fonts/Arial.ttf", 12)

# First define some constants to allow easy resizing of shapes. 
# (taken from Adafruits stats.py)
padding = -2
top = padding
bottom = height-padding

# Move left to right keeping track of the current x 
# position for drawing shapes. (taken from Adafruits stats.py)
x = 0

# Command we'll use to grab the IP of the Pi. (taken from Adafruits stats.py)
cmd = "hostname -I | cut -d\' \' -f1"

# "subprocess" to output above command. (taken from Adafruits stats.py)
IP = subprocess.check_output(cmd, shell = True )

# Regular expression to strip the IP retrieved in the above command 
# of extra characters.
IP = re.search( r'[0-9]+(?:\.[0-9]+){3}', str(IP) ).group()

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def ListMessagesMatchingQuery(service, user_id, query=''):
	"""List all Messages of the user's mailbox matching the query.

	Args:
		service: Authorized Gmail API service instance.
		user_id: User's email address. The special value "me"
		can be used to indicate the authenticated user.
		query: String used to filter messages returned.
		Eg.- 'from:[email protected]_domain.com' for Messages from a particular sender.

	Returns:
		List of Messages that match the criteria of the query. Note that the
		returned list contains Message IDs, you must use get with the
		appropriate ID to get the details of a Message.
	"""
	try:
		response = service.users().messages().list(userId=user_id, q=query).execute()
		messages = []
		if 'messages' in response:
			messages.extend(response['messages'])

			while 'nextPageToken' in response:
				page_token = response['nextPageToken']
				response = service.users().messages().list(userId=user_id, q=query, \
					pageToken=page_token).execute()
				messages.extend(response['messages'])

		return messages
	except:
		print('An error occurred:')

def main():
	global TEXT_TO_APPEND_TO_COUNT
	"""Shows basic usage of the Gmail API.
	Lists the user's Gmail labels.
	"""
	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_local_server(port=0)
		# Save the credentials for the next run
		with open('token.pickle', 'wb') as token:
			pickle.dump(creds, token)

	service = build('gmail', 'v1', credentials=creds)
	
	# Get the email count by calling the new function "ListMessagesMatchingQuery()". 
	# Use the "len()" method which will give you the sum of the list elements that 
	# are returned (total unread emails). Then, wrap it all in a "try" statement
	# so that the script doesn't break if "NoneType" is returned.
	try:
		email_count = len(ListMessagesMatchingQuery(service, "me", GMAIL_QUERY_ARGS))
	except TypeError as e:	
		print("Received and error:", e)
        
    # If the email unread messages count is 1 and the string ends with an "s", 
	# remove the "s" from the end of the string.
	text_modified = TEXT_TO_APPEND_TO_COUNT
	if (int(email_count) is 1 and TEXT_TO_APPEND_TO_COUNT.endswith('s')):
		text_modified = TEXT_TO_APPEND_TO_COUNT[:-1]

	# Create a new instance of the pyowm wrapper with your API key.
	owm = pyowm.OWM(OPEN_WEATHER_MAP_API_KEY)  

	# Search for current weather in OPEN_WEATHER_MAP_LOCATION
	observation = owm.weather_at_place(OPEN_WEATHER_MAP_LOCATION)
	w = observation.get_weather()
	# Get the current temperature.
	temperature = round(w.get_temperature(OPEN_WEATHER_MAP_TEMP_SCALE)['temp'])
	# Get the current weather info
	sky = w.get_detailed_status()
	# Create two variables that will store tomorrows values.
	tomorrows_sky = ''
	tomorrows_temp = ''

	# Create a new datetime & add 1 day. 
	current_time = datetime.datetime.now()
	current_time_adjusted = current_time + datetime.timedelta(days=+1)

	# Get the forecast for the next few days in 3 hour time blocks.
	fc = owm.three_hours_forecast(OPEN_WEATHER_MAP_LOCATION)
	f = fc.get_forecast()
	# Loop through the forecast list.
	for weather in f:
		# Get the time info for the current element in the weather forecast list.
		ref_time = weather.get_reference_time('date').replace(tzinfo=None)
		# Next, find the closest time that matches this time tomorrow 
		compare = current_time_adjusted > ref_time
		if(compare is False):
			tomorrows_sky = weather.get_detailed_status()
			tomorrows_temp = round(weather.get_temperature(OPEN_WEATHER_MAP_TEMP_SCALE)['temp'])
			print(weather.get_temperature(OPEN_WEATHER_MAP_TEMP_SCALE))
			print("tomorrows_sky", tomorrows_sky)
			print("tomorrows_temp", tomorrows_temp)
			print("weather.get_reference_time('iso')", weather.get_reference_time('iso'))
			break

	print("temperature:", temperature)
	print("sky:", sky)

	# Draw a black filled box to clear the image.  (taken from Adafruits stats.py)
	draw.rectangle((0,0,width,height), outline=0, fill=0)

	# Draw each line of text to the display using the fonts we defined above  
	# (taken from Adafruits stats.py)
	draw.text((x, top),       str(email_count)+" "+text_modified,  font=font, fill=255)
	draw.text((x, top+19),    str(temperature)+"°"+" / "+str(tomorrows_temp)+"°",  font=font, fill=255)
	draw.text((x, top+38),    str(sky.title() + " / " + tomorrows_sky.title()),  font=font_sky, fill=255)
	draw.text((x, top+54),    "IP: " + str(IP),  font=font_email, fill=255)

	# Display image. (taken from Adafruits stats.py)
	disp.image(image)
	disp.display()

	# Display the results to the console for debugging.
	print("Email count:", email_count)

if __name__ == '__main__':
	while True:
		# Create a variable to hold the "now" (current) time
		current_time = datetime.datetime.now()
		# We will pre-populate the following two variables with the 
		# current time as well. In a few steps, we'll also modify these 
		# variables to reflect the "SCREENSAVER_TIME_BEGIN" value,
		# and the "SCREENSAVER_TIME_END" value.
		t1_begin = datetime.datetime.now()
		t2_end = datetime.datetime.now()

		# To use the values for the screensaver begin/end times,
		# we'll "split" the strings by the ":" character so that
		# each time element: hour/minute/second will become individual
		# strings in an array.
		t1_begin_list = SCREENSAVER_TIME_BEGIN.split(":") # "05:30:00" -> ["05", "30", "00"]
		t2_end_list = SCREENSAVER_TIME_END.split(":") # "08:00:00" -> ["08", "00", "00"]

		# Now we will update the "t1_begin" & "t2_end" times to reflect
		# the user specified times.
		# The variables are wrapped in an "int()" function to 
		# change the values to integers which effectively removes any
		# leading zero's as well.
		t1_begin = t1_begin.replace(hour=int(t1_begin_list[0]), minute=int(t1_begin_list[1]), second=int(t1_begin_list[2]))
		t2_end = t2_end.replace(hour=int(t2_end_list[0]), minute=int(t2_end_list[1]), second=int(t2_end_list[2]))

		# Find the difference between the two times 
		# (end time minus begin time).
		time_delta = t2_end - t1_begin

		# After creating these variables above, there 
		# is nothing telling the program that the "t2_end" value
		# is supposed to be in the future. So let's check if the  
		# "time_delta" value from above is negative, if so, add 
		# a day to the "t2_end" time var and create a new variable
		# to hold the resulting value.
		if(time_delta.total_seconds() < 0):
			t2_end_adjusted = t2_end + datetime.timedelta(days=+1)
		else:
			t2_end_adjusted = time_delta + t1_begin

		# If: the current time is less than or equal to the screensaver "BEGIN" time OR
		# If: the current time is more than or equal to the newly "adjusted" screensaver "END" time...run
		# Else: draw a black image on the screen.
		if(current_time <= t1_begin or current_time >= t2_end_adjusted):
			
			# Sometimes the server is not accessible for whatever reason. 
			# Let's catch that and any other error and print something
			# to the console.
			try:
				# Run the main app.
				main()
			except:
				# Print that there was an error:
				print("Encountered an error.")

		else:

			# Draw a black filled box to clear the image.
			draw.rectangle((0,0,width,height), outline=0, fill=0)

			# Display image.
			disp.image(image)
			disp.display()
			print("Screensaver Mode - sleeping.")
	
		# Sleep for the configured delay time.
		time.sleep(DELAY_TIME_TO_QUERY_API)

Run it:

% python3 quickstart.py

You should have something similar to this (and hopefully a bit cooler):

Note: I’m uncertain about all of the possible weather output “conditions”. Above you see, “Clear Sky / Clear Sky” which fits nicely. It may be necessary to scale down that font size to fit the weather conditions on one line.

That’s it.

Conclusion

This is a dorky project, but fairly useful. Everyone has a Raspberry Pi, and possibly extra OLED display’s laying around (if not, these things are like a dollar a piece – if you look hard enough). It’s a little work to write the code and set things up, but if set aside a weekend, and follow along closely – you’ll end up with a device that continues to work well without needing to mess with it. Make it look better – take it to the finish line and house it in a nice 3d printed enclosure or wooden box of some kind. Try it out.

Leave a Reply

Your email address will not be published. Required fields are marked *