Open Weather Map API Key

We'll be using OpenWeatherMap.org to retrieve the weather info through its API. In order to do so, you'll need to register for an account and get your API key.

Go to this link and register for a free account. Once registered, you'll get an email containing your API key, also known as the "openweather token".

Python Code

The code for this project was based off the code available in the PyPortal Weather Station guide, though the graphics portion of this project works quite differently. Go ahead and download the project and we'll go over the code.

To start with, look for this line in the code and be sure to enter your OPEN_WEATHER_TOKEN and set LOCATION variable to your location:

Download: file
# You'll need to get a token from openweathermap.org, looks like:
# 'b6907d289e10d714a6e88b30761fae22'
OPEN_WEATHER_TOKEN = ""

Next, look for this line in the code and set the LOCATION variable to your location:

Download: file
# Use cityname, country code where countrycode is ISO3166 format.
# E.g. "New York, US" or "London, GB"
LOCATION = "Manhattan, US"

To run the example, upload weather.py, weather_graphics.py, and meteocons.ttf to the same directory and use the following command:

python3 weather.py

You should see an output similar to this:

Automation Changes

If you would like to run the weather script in a Cron job or to run on start up, you may find it easier to move the meteocons.ttf file to a more central place.

A good place to put it would be to create a folder named meteocons inside of in /usr/share/fonts/truetype and then move the meteocons.ttf file inside of that folder.

You'll also need to change the location of the file inside of weather_graphics.py.

How It Works

There are two files that are used. Let's start by going over the main file, which is the one you will use to to run the example.

Main File

First we start by loading any libraries that are used. The notable libraries are urllib, adafruit_epd, weather_graphics. The library called urllib is a built-in library used for retrieving and parsing data from the internet. The adafruit_epd library is used to initialize and write data to the ePaper Display, and the weather_graphics is the portion of code that will handle any graphics, which we'll go over next.

Download: file
import time
import urllib.request
import urllib.parse
import digitalio
import busio
import board
from adafruit_epd.ssd1675 import Adafruit_SSD1675
from weather_graphics import Weather_Graphics

Next, we setup SPI and any pins used. If you are using the EInk bonnet or have wired it up like in the setup page, you shouldn't need to change anything unless you are using a different board than the Raspberry Pi.

Download: file
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)

The next section contains the parameters used for connecting to Open Weather. You should have already filled in the token and location, and if not, take a look at the beginning of this page.

Download: file
# You'll need to get a token from openweathermap.org, looks like:
# 'b6907d289e10d714a6e88b30761fae22'
OPEN_WEATHER_TOKEN = ""

# Use cityname, country code where countrycode is ISO3166 format.
# E.g. "New York, US" or "London, GB"
LOCATION = "Manhattan, US"
DATA_SOURCE_URL = "http://api.openweathermap.org/data/2.5/weather"

The next section just checks to make sure you've added the token and assembles the URL with the parameters.

Download: file
if len(OPEN_WEATHER_TOKEN) == 0:
    raise RuntimeError(
        "You need to set your token first. If you don't already have one, you can register for a free account at https://home.openweathermap.org/users/sign_up"
    )

# Set up where we'll be fetching data from
params = {"q": LOCATION, "appid": OPEN_WEATHER_TOKEN}
data_source = DATA_SOURCE_URL + "?" + urllib.parse.urlencode(params)

After that, we setup the ePaper display and set the rotation.

Download: file
# Initialize the Display
display = Adafruit_SSD1675(
    122, 250, spi, cs_pin=ecs, dc_pin=dc, sramcs_pin=None, rst_pin=rst, busy_pin=busy,
)

display.rotation = 1

Next we initialize the Graphics library and initialize the weather_refresh variable with a default value. The weather_refresh variable is used to determine the last time that the weather data was refreshed so that we can have 2 different timing loops for weather and time.

Download: file
gfx = Weather_Graphics(display, am_pm=True, celsius=False)
weather_refresh = None

Finally, we have the main code loop. It checks to see if the weather data has been retrieved at all or that at least 600 seconds (10 minutes) have passed since the last time it was refreshed. If either condition is met, it uses urllib to get the data from the Open Weather URL. It then updates the time (as well as the display) using the weather graphics and waits for 300 seconds (5 minutes) so that the display isn't refreshed too often, which can cause the eInk displays to fail prematurely.

Download: file
while True:
    # only query the weather every 10 minutes (and on first run)
    if (not weather_refresh) or (time.monotonic() - weather_refresh) > 600:
        response = urllib.request.urlopen(data_source)
        if response.getcode() == 200:
            value = response.read()
            print("Response is", value)
            gfx.display_weather(value)
            weather_refresh = time.monotonic()
        else:
            print("Unable to retrieve data at {}".format(url))

    gfx.update_time()
    time.sleep(300)  # wait 5 minutes before updating anything again

Weather Graphics Library

Next we'll go over the graphics file. First we start by importing any libraries that we'll be using. The notable library this time is PIL or the Python Imaging library, which is used to handle all the font rendering.

Download: file
from datetime import datetime
import json
from PIL import Image, ImageDraw, ImageFont
from adafruit_epd.epd import Adafruit_EPD

Next, we set up the fonts. If you would like to change them, you can do so here. Each font is a combination of the font file and the font size in points.

Download: file
small_font = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16
)
medium_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
large_font = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24
)
icon_font = ImageFont.truetype("./meteocons.ttf", 48)

Next we create an icon map, which will map the specified OpenWeather icon code to the font character that the meteocons font uses. If you wanted to change the icons, this would be the place to make changes.

Download: file
# Map the OpenWeatherMap icon code to the appropriate font character
# See http://www.alessioatzeni.com/meteocons/ for icons
ICON_MAP = {
    "01d": "B",
    "01n": "C",
    "02d": "H",
    "02n": "I",
    "03d": "N",
    "03n": "N",
    "04d": "Y",
    "04n": "Y",
    "09d": "Q",
    "09n": "Q",
    "10d": "R",
    "10n": "R",
    "11d": "Z",
    "11n": "Z",
    "13d": "W",
    "13n": "W",
    "50d": "J",
    "50n": "K",
}

Next we define a couple of colors to make the code more readable:

Download: file
# RGB Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

Now we have the actual Weather_Graphics class:

Download: file
class Weather_Graphics:

The first function in the Weather Graphics is code to initialize any variables that we'll be storing to default values including setting the fonts that we set up earlier.

Download: file
def __init__(self, display, *, am_pm=True, celsius=True):
    self.am_pm = am_pm
    self.celsius = celsius

    self.small_font = small_font
    self.medium_font = medium_font
    self.large_font = large_font

    self.display = display

    self._weather_icon = None
    self._city_name = None
    self._main_text = None
    self._temperature = None
    self._description = None
    self._time_text = None

Next we have the display_weather function, which all it does is format the data that is passed in into something more readable to humans. As it does, it prints the value to the console to make debugging easier. Once everything is set, it calls the function to update the time.

Download: file
def display_weather(self, weather):
    weather = json.loads(weather.decode("utf-8"))

    # set the icon/background
    self._weather_icon = ICON_MAP[weather["weather"][0]["icon"]]

    city_name = weather["name"] + ", " + weather["sys"]["country"]
    print(city_name)
    self._city_name = city_name

    main = weather["weather"][0]["main"]
    print(main)
    self._main_text = main

    temperature = weather["main"]["temp"] - 273.15  # its...in kelvin
    print(temperature)
    if self.celsius:
        self._temperature = "%d °C" % temperature
    else:
        self._temperature = "%d °F" % ((temperature * 9 / 5) + 32)

    description = weather["weather"][0]["description"]
    description = description[0].upper() + description[1:]
    print(description)
    self._description = description
    # "thunderstorm with heavy drizzle"

    self.update_time()

The update_time function is pretty short. It just retrieves the time from the linux operating system using the datetime library, then it formats and sets the value to one of the class variables. Once that is done, it calls the update_display function.

Download: file
def update_time(self):
    now = datetime.now()
    self._time_text = now.strftime("%I:%M %p").lstrip("0").replace(" 0", " ")
    self.update_display()

The last function in the library is the update_display function, which is used to take the values of each of the class variables and draw them to a canvas. PIL's Image.new() function is used to create a canvas the size of the display and then the draw.text() function is used to draw the text to the canvas in various locations. Once all the text is drawn, the canvas is passed into the EPD library with the self.display.image() function and then the display is refreshed with the self.display.display() function.

Download: file
def update_display(self):
    self.display.fill(Adafruit_EPD.WHITE)
    image = Image.new("RGB", (self.display.width, self.display.height), color=WHITE)
    draw = ImageDraw.Draw(image)

    # Draw the Icon
    (font_width, font_height) = icon_font.getsize(self._weather_icon)
    draw.text(
        (
            self.display.width // 2 - font_width // 2,
            self.display.height // 2 - font_height // 2 - 5,
        ),
        self._weather_icon,
        font=icon_font,
        fill=BLACK,
    )

    # Draw the city
    draw.text(
        (5, 5), self._city_name, font=self.medium_font, fill=BLACK,
    )

    # Draw the time
    (font_width, font_height) = medium_font.getsize(self._time_text)
    draw.text(
        (5, font_height * 2 - 5),
        self._time_text,
        font=self.medium_font,
        fill=BLACK,
    )

    # Draw the main text
    (font_width, font_height) = large_font.getsize(self._main_text)
    draw.text(
        (5, self.display.height - font_height * 2),
        self._main_text,
        font=self.large_font,
        fill=BLACK,
    )

    # Draw the description
    (font_width, font_height) = small_font.getsize(self._description)
    draw.text(
        (5, self.display.height - font_height - 5),
        self._description,
        font=self.small_font,
        fill=BLACK,
    )

    # Draw the temperature
    (font_width, font_height) = large_font.getsize(self._temperature)
    draw.text(
        (
            self.display.width - font_width - 5,
            self.display.height - font_height * 2,
        ),
        self._temperature,
        font=self.large_font,
        fill=BLACK,
    )

    self.display.image(image)
    self.display.display()

Full Example Code

The first file is the main file:

"""
This example queries the Open Weather Maps site API to find out the current
weather for your location... and display it on a eInk Bonnet!
"""

import time
import urllib.request
import urllib.parse
import digitalio
import busio
import board
from adafruit_epd.ssd1675 import Adafruit_SSD1675
from weather_graphics import Weather_Graphics

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)

# You'll need to get a token from openweathermap.org, looks like:
# 'b6907d289e10d714a6e88b30761fae22'
OPEN_WEATHER_TOKEN = ""

# Use cityname, country code where countrycode is ISO3166 format.
# E.g. "New York, US" or "London, GB"
LOCATION = "Manhattan, US"
DATA_SOURCE_URL = "http://api.openweathermap.org/data/2.5/weather"

if len(OPEN_WEATHER_TOKEN) == 0:
    raise RuntimeError(
        "You need to set your token first. If you don't already have one, you can register for a free account at https://home.openweathermap.org/users/sign_up"
    )

# Set up where we'll be fetching data from
params = {"q": LOCATION, "appid": OPEN_WEATHER_TOKEN}
data_source = DATA_SOURCE_URL + "?" + urllib.parse.urlencode(params)

# Initialize the Display
display = Adafruit_SSD1675(
    122, 250, spi, cs_pin=ecs, dc_pin=dc, sramcs_pin=None, rst_pin=rst, busy_pin=busy,
)

display.rotation = 1

gfx = Weather_Graphics(display, am_pm=True, celsius=False)
weather_refresh = None

while True:
    # only query the weather every 10 minutes (and on first run)
    if (not weather_refresh) or (time.monotonic() - weather_refresh) > 600:
        response = urllib.request.urlopen(data_source)
        if response.getcode() == 200:
            value = response.read()
            print("Response is", value)
            gfx.display_weather(value)
            weather_refresh = time.monotonic()
        else:
            print("Unable to retrieve data at {}".format(url))

    gfx.update_time()
    time.sleep(300)  # wait 5 minutes before updating anything again

The other file that you will need is the graphics file:

from datetime import datetime
import json
from PIL import Image, ImageDraw, ImageFont
from adafruit_epd.epd import Adafruit_EPD

small_font = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16
)
medium_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
large_font = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24
)
icon_font = ImageFont.truetype("./meteocons.ttf", 48)

# Map the OpenWeatherMap icon code to the appropriate font character
# See http://www.alessioatzeni.com/meteocons/ for icons
ICON_MAP = {
    "01d": "B",
    "01n": "C",
    "02d": "H",
    "02n": "I",
    "03d": "N",
    "03n": "N",
    "04d": "Y",
    "04n": "Y",
    "09d": "Q",
    "09n": "Q",
    "10d": "R",
    "10n": "R",
    "11d": "Z",
    "11n": "Z",
    "13d": "W",
    "13n": "W",
    "50d": "J",
    "50n": "K",
}

# RGB Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)


class Weather_Graphics:
    def __init__(self, display, *, am_pm=True, celsius=True):
        self.am_pm = am_pm
        self.celsius = celsius

        self.small_font = small_font
        self.medium_font = medium_font
        self.large_font = large_font

        self.display = display

        self._weather_icon = None
        self._city_name = None
        self._main_text = None
        self._temperature = None
        self._description = None
        self._time_text = None

    def display_weather(self, weather):
        weather = json.loads(weather.decode("utf-8"))

        # set the icon/background
        self._weather_icon = ICON_MAP[weather["weather"][0]["icon"]]

        city_name = weather["name"] + ", " + weather["sys"]["country"]
        print(city_name)
        self._city_name = city_name

        main = weather["weather"][0]["main"]
        print(main)
        self._main_text = main

        temperature = weather["main"]["temp"] - 273.15  # its...in kelvin
        print(temperature)
        if self.celsius:
            self._temperature = "%d °C" % temperature
        else:
            self._temperature = "%d °F" % ((temperature * 9 / 5) + 32)

        description = weather["weather"][0]["description"]
        description = description[0].upper() + description[1:]
        print(description)
        self._description = description
        # "thunderstorm with heavy drizzle"

        self.update_time()

    def update_time(self):
        now = datetime.now()
        self._time_text = now.strftime("%I:%M %p").lstrip("0").replace(" 0", " ")
        self.update_display()

    def update_display(self):
        self.display.fill(Adafruit_EPD.WHITE)
        image = Image.new("RGB", (self.display.width, self.display.height), color=WHITE)
        draw = ImageDraw.Draw(image)

        # Draw the Icon
        (font_width, font_height) = icon_font.getsize(self._weather_icon)
        draw.text(
            (
                self.display.width // 2 - font_width // 2,
                self.display.height // 2 - font_height // 2 - 5,
            ),
            self._weather_icon,
            font=icon_font,
            fill=BLACK,
        )

        # Draw the city
        draw.text(
            (5, 5), self._city_name, font=self.medium_font, fill=BLACK,
        )

        # Draw the time
        (font_width, font_height) = medium_font.getsize(self._time_text)
        draw.text(
            (5, font_height * 2 - 5),
            self._time_text,
            font=self.medium_font,
            fill=BLACK,
        )

        # Draw the main text
        (font_width, font_height) = large_font.getsize(self._main_text)
        draw.text(
            (5, self.display.height - font_height * 2),
            self._main_text,
            font=self.large_font,
            fill=BLACK,
        )

        # Draw the description
        (font_width, font_height) = small_font.getsize(self._description)
        draw.text(
            (5, self.display.height - font_height - 5),
            self._description,
            font=self.small_font,
            fill=BLACK,
        )

        # Draw the temperature
        (font_width, font_height) = large_font.getsize(self._temperature)
        draw.text(
            (
                self.display.width - font_width - 5,
                self.display.height - font_height * 2,
            ),
            self._temperature,
            font=self.large_font,
            fill=BLACK,
        )

        self.display.image(image)
        self.display.display()

This guide was first published on Jul 15, 2020. It was last updated on Jul 15, 2020.

This page (Weather Station Code) was last updated on Nov 23, 2020.