Gmail total unread messages display powered by a Raspberry Pi Zero + OLED.

I’ve wanted to create this thing for a while and finally I went for it.

In the following blog post I’ll retrace my steps as I:

  • Ran through the authentication process for the Gmail API and created a “quickstart.py” example script.
  • Edited the “quickstart.py” script to retrieve the total unread messages count.
  • Wired up a Raspberry Pi Zero to an OLED display to eventually display the unread email count.
  • Moved the “quickstart.py” script and associated auth files to the Pi where I let it run, polling for unread messages and displaying the results on the OLED in front of me as I work.

1) Run through the official Gmail-API-for-python tutorial and set up the “quickstart.py” script to get authenticated via cli.

Step through this google tutorial entirely making sure you get the “quickstart.py” script to fully work as described. Note: In stepping through this google tut you will also generate both a “credentials.json” file and a “token.pickle” file – both are needed for authentication with the api:

https://developers.google.com/gmail/api/quickstart/python

2) Tweak the “quickstart.py ” you just made.

Following the previous tutorial, let’s add a function to grab emails based on special query args like “label:unread category:primary“. Later we’ll add the OLED logic to this script too.

Add a new function to “quickstart.py” taken from the official docs:

https://developers.google.com/gmail/api/v1/reference/users/messages/list

This is the new function we’re going to use:

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:')

Grab the unread email count by calling the new function with some query arguments. We’ll add this toward the bottom:

email_count = len(ListMessagesMatchingQuery(service, "me", "label:unread category:primary"))

Here is the full code so far with the additions:

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

# 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():
    """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)
    
    email_count = len(ListMessagesMatchingQuery(service, "me", "label:unread category:primary"))

    print("Email count:", email_count)     

if __name__ == '__main__':
    main()

Try it out:

% python3 quickstart.py

It should print the unread email total in your console:

It worked! 3 unread emails. Now let’s assemble the Pi and install the utilities/libs we’ll be using to operate the OLED and to eventually display the unread Gmail messages count on it.

3) Items to Buy / Assembly:

# List of items needed to build this project (*info: most links in the list below link to Amazon items through my Amazon Affiliate account. Feel free to shop around for better prices!):

Here are the items on my table (power adapter/tools not shown):

128×64 OLED display – Raspberry Pi Zero W – SD card
Double-checking pins before I solder.

# Wire up the OLED to the newly soldered Pi.

GND -> GND

3.3v -> 3.3v

SDA -> SDA

SCL -> SCL

4) Setup your Pi with Raspian, connect it to Wifi, and be sure you can SSH into it.

There are many tutorials out there on how to do this. I usually choose the “headless” Raspian image and prepare the SD card with my wifi creds and enable SSH using this tutorial:

https://styxit.com/2017/03/14/headless-raspberry-setup.html

…this way you don’t have to fool around with hooking up a monitor, mouse, and keyboard.

If using the above tut and installing headless, use the following for setting up your wifi conf in Raspian Stretch & later to get connected to wifi headlessly:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
network={
    ssid="YOUR_SSID"
    psk="YOUR_WIFI_PASSWORD"
    key_mgmt=WPA-PSK
}

5) Set up the Raspberry Pi to interact with the OLED display via I2C & install the necessary libraries.

Enable the I2C interface using “raspi-config”:

% sudo raspi-config
raspi-config – Interfacing Options
raspi-config – Interfacing Options – PS I2C
raspi-config – Interfacing Options – PS I2C – confirm

Update/upgrade, then install the required libraries on the Pi:

% sudo apt -y update
% sudo apt -y upgrade
% sudo apt install -y python3-dev python3-pil python3-pip python3-setuptools python3-rpi.gpio

Install git and then clone the Adafruit OLED Python Library:

% sudo apt install -y git
% git clone https://github.com/adafruit/Adafruit_Python_SSD1306.git

Change directories and Install it:

% cd Adafruit_Python_SSD1306
% sudo python3 setup.py install

With everything installed, test it out using one of the Adafruit OLED examples:

% cd ./examples
% python3 ./stats.py

Success! The display/libs are working:

…running Adafruit’s “stats.py”

So far so good. “stats.py” is kinda rad. Let’s use some of the logic in it for our own purposes.

6) Move the “quickstart.py” script (and auth files) to the Pi, add some more code to interact with the OLED, and test it out.

Move the “quickstart.py” to a folder in your “/home/pi” directory. Also, be sure to move the associated authentication files: “credentials.json” & “token.pickle” that were created when you ran through the Google tutorial at the start. We’re still editing the “quickstart.py” so keep that in mind.

Note: If you’re skimming this, you must run through the initial Gmail API “quickstart” tutorial on your local machine (with a web browser), and later move the necessary files to the Pi – like we’re doing now. Also, you need to install all the Gmail API pip python libraries used for this on the fresh Pi.

Borrowing heavily from that “stats.py” script we just ran, I’ll start by adding the Adafruit libs & some other helper libs we’ll be using toward the top of “quickstart.py” (full code further down):

# 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

Next, let’s fill it out by adding some vars, setting some values, etc.:

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
EMAIL_ADDRESS_TEXT_LINE = "[email protected]"
TEXT_TO_APPEND_TO_COUNT = "Unread Emails"
DELAY_TIME_TO_QUERY_API = 10 # seconds
### 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. (taken from Adafruits stats.py)
font = ImageFont.load_default()

# 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()

Further down in “quickstart.py”, we’ll add the code to display the unread messages count (and other text) on the OLED display hooked up to the Pi:

    # 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).
	email_count = len(ListMessagesMatchingQuery(service, "me", GMAIL_QUERY_ARGS))
        
    # 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]

	# 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+38),    EMAIL_ADDRESS_TEXT_LINE,  font=font, fill=255)
	draw.text((x, top+50),    "IP: " + str(IP),  font=font, 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)

Now let’s add a while loop to run the script forever. Also, we’ll add a 10-second delay timer to limit how often it calls the API:

if __name__ == '__main__':
	# Run the app over and over unless you manually stop it or unless it stops
	# due to an error.
	while True:
		# 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.")

		# Sleep for the configured delay time.
		time.sleep(DELAY_TIME_TO_QUERY_API)

Here is the full “quickstart.py” with all the edits/additions we’ve made so far:

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 error handling.
import httplib2

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
EMAIL_ADDRESS_TEXT_LINE = "[email protected]"
TEXT_TO_APPEND_TO_COUNT = "Unread Emails"
DELAY_TIME_TO_QUERY_API = 10 # seconds
### 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. (taken from Adafruits stats.py)
font = ImageFont.load_default()

# 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).
	email_count = len(ListMessagesMatchingQuery(service, "me", GMAIL_QUERY_ARGS))
        
    # 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]

	# 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+38),    EMAIL_ADDRESS_TEXT_LINE,  font=font, fill=255)
	draw.text((x, top+50),    "IP: " + str(IP),  font=font, 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__':
	# Run the app over and over unless you manually stop it or unless it stops
	# due to an error.
	while True:
		# 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.")

		# Sleep for the configured delay time.
		time.sleep(DELAY_TIME_TO_QUERY_API)
	

Run it:

% python3 quickstart.py

Your OLED should look similar to mine:

OLED display hooked up to a Raspberry Pi Zero, displaying total gmail unread messages count.

That’s not right. It should look better.

It’s usable like this, but let’s add a font to make it look a little nicer. I went with “Arial”, and downloaded the “Arial.ttf” font from this site:

https://www.ffonts.net/arial.font

Unzip that and put the “Arial.ttf” font file in the same directory as your “quickstart.py” script. I put mine in a subdirectory named “fonts”.

Find the line that looks like this:

# Load default font.
font = ImageFont.load_default()

Comment it out and use the new font instead:

# Load default font.
#font = ImageFont.load_default()
font = ImageFont.truetype("fonts/Arial.ttf", 16)
font_email = ImageFont.truetype("fonts/Arial.ttf", 11) # reduced font size for the email line

Find the code that updates the OLED with text. Make it look like this:

    # 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+38),    EMAIL_ADDRESS_TEXT_LINE,  font=font_email, fill=255)
	draw.text((x, top+50),    "IP: " + str(IP),  font=font, fill=255)

The full “quickstart.py”:

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

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
EMAIL_ADDRESS_TEXT_LINE = "[email protected]"
TEXT_TO_APPEND_TO_COUNT = "Unread Emails"
DELAY_TIME_TO_QUERY_API = 10 # seconds
### 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)

# 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).
	email_count = len(ListMessagesMatchingQuery(service, "me", GMAIL_QUERY_ARGS))
        
    # 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]

	# 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+38),    EMAIL_ADDRESS_TEXT_LINE,  font=font_email, fill=255)
	draw.text((x, top+50),    "IP: " + str(IP),  font=font, 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__':
	# Run the app over and over unless you manually stop it or unless it stops
	# due to an error.
	while True:
		# 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.")

		# Sleep for the configured delay time.
		time.sleep(DELAY_TIME_TO_QUERY_API)
	

If the font is loading and everything is working, it should look like this:

Gmail Unread Email Counter - Right in Front of Me.
Pseudo-Blog.com - Raspberry Pi OLED Gmail unread email messages counter project.

Run it forever

Now let’s wrap this up by using a trick to run “quickstart.py” forever.

We’ll use the “screen” utility to keep the script running. First, install “screen”.

% sudo apt install -y screen

Then run screen:

% screen

Run the script:

% python3 ./quickstart.py

Now “quickstart.py” will run “forever” checking for unread emails every 10 seconds (or whatever delay time you set in quickstart.py). Keep the console open now for debugging, but you can hit “C+a d” to detach from screen and keep the process running – that’s ultimately the idea. (screen docs) – also, feel free to rename “quickstart.py” to something better at this point.

Run it on reboot

Create a crontab to restart the above command on reboot (replacing my paths & file name with yours):

% crontab -e
@reboot cd /home/pi/projects/gmail-email-counter/ && screen -d -m python3 ./quickstart.py

Now it will run forever – even on reboot.

*For multiple Gmail accounts: here is a version of “quickstart.py” you can use if you want to display the sum total unread emails of multiple accounts: gist.github/pseudo-projects/…

…just run through the “quickstart” Gmail authentication steps for each account to generate the corresponding “credentials.json” & “token.pickle” files and put them all in the same directory as the new “gmail-total-unread-emails-multi-pi-oled.py” file. Update the code with the names of each associated auth files. More info in the comments in the above python script.

Final Thoughts

In a previous post, I discussed how I built this same project using the “esp8266-01” and a self-hosted MQTT “broker”. I’ll probably go back to the esp8266 for this so I can repurpose the Pi. But I do like the new font used in this one. We’ll see.

Going Further – Screensaver Mode

Apparently, these OLED devices have an issue with burn-in if the display doesn’t change often. Let’s add some functionality to blank out the display during times we’re not around to use it.

Start by adding the “datetime” library to “quickstart.py”:

import datetime

Next, let’s add more “editable” vars toward the top:

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

The following is the full “main” loop updated with the logic, using the variables above, to “sleep” during the specified hours:

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)

Here is the full code with all the additions + the “screensaver” 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

### BEGIN Editable Vars
GMAIL_QUERY_ARGS = "label:unread category:primary"
EMAIL_ADDRESS_TEXT_LINE = "[email protected]"
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" 
### 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)

# 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).
	email_count = len(ListMessagesMatchingQuery(service, "me", GMAIL_QUERY_ARGS))
        
    # 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]

	# 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+38),    EMAIL_ADDRESS_TEXT_LINE,  font=font_email, fill=255)
	draw.text((x, top+50),    "IP: " + str(IP),  font=font, 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)

That should do it. With those final additions, you should have a blank screen during the configured hours. Mine shut off at midnight.

Pseudo-Blog.com - Screensaver mode.

Going Even Further

A continuation to this project that displays local weather info along with the Gmail unread messages count can be found here: https://pseudo-server.com/blog/gmail-total-unread-messages-display-local-weather-info-raspberry-pi-zero-oled/.

Leave a Reply

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