Back when I first got a Google Home Mini, I was spending way too much time in terminal and I slowly started replacing commands I’d use on the Google device with commands on my MacBook, because, as always, why not?
The title says it all. I can now run a weather command in terminal to grab the weather of my home town, including wind speed / direction, temperature and general description (clouds, rain, snow, sun). It’s a pretty crude program and it’ll probably crash if you specify an unknown location, but thats okay. Don’t specify an unknown location and it’ll be fine!
This was the perfect excuse to brush up on a bit of python at the time. Although I threw this code together quickly, the functionality I sought is there, and the real reason behind it is because I’ve been neglecting Python for too long. All the years I’ve been writing code, I’ve barely touched this language. Because I started with PHP and then C++, I am very comfortable with a C-style syntax, so when indentation and colons become the norm and semi-colons don’t exist, its very foreign to me. So, the more I write, the more comfortable I get.
Use these links to quickly jump to the sections below
Defining Some Essential Parameters
Making the Request
Parsing the Data
Printing the Data
The Full Code
The Full Code Updated
First of all, I needed to find a free weather API provider. Easy stuff, thanks Google. OpenWeather provide just that. Of course there are limited API requests, but I don’t think I will be exceeding those any time soon. From there on, it’s just as easy as sending a GET request to fetch some JSON and then parsing it to be displayed how I see fit.
The only technicality involved was converting a wind direction from degrees to an actual direction. But a spot of math and a big List later, simple.
I had to make an account on https://openweathermap.org which is so self-explanatory I won’t even entertain explaining it. It takes a few hours for your API key to become active though, I kind of forgot until the evening so I don’t know exactly how long it takes.
Once thats done, take a look at their API documentation to see what kind of requests you can make. I just went with the first one, get current data by city name.
What the documentation doesn’t say is that you need to include your API key as a parameter with the name appid
So let’s get to writing some python code!
Defining Some Essential API Parameters
We need our API endpoint, API key and some additional parameters to tell the API which units we want, and the location we want to query for. That’s what we are defining here. We declare and initialise these variables after our imports. We also define our
# api-endpoint
URL = "https://api.openweathermap.org/data/2.5/weather"
location = "london"
api_key = "API_KEY"
units = "metric"
temp_unit = "celsius"
PARAMS = {'q': location, 'appid': api_key, 'units': units}
direction_list = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N"]
Making the Request
Next, we make the request to the OpenWeather API using the python requests library, and pass our params along with the request. It’s a good idea to see what data the request returns before deciding how you want to format the data for output, so we’ll do that here too. Note that this won’t be in the final script, however. Also, not all data fields are guaranteed.
req = requests.get(url=URL, params=PARAMS)
print(req.json())
This will return some output that looks something like this:
{
'coord': {
'lon': -0.1257,
'lat': 51.5085
},
'weather': [
{
'id': 500,
'main': 'Rain',
'description': 'light rain',
'icon': '10n'
}
],
'base': 'stations',
'main': {
'temp': 17.56,
'feels_like': 17.7,
'temp_min': 16.49,
'temp_max': 18.23,
'pressure': 1005,
'humidity': 89
},
'visibility': 10000,
'wind': {
'speed': 5.66,
'deg': 240
},
'rain': {
'1h': 0.15
},
'clouds': {
'all': 100
},
'dt': 1688161574,
'sys': {
'type': 2,
'id': 2075535,
'country': 'GB',
'sunrise': 1688096796,
'sunset': 1688156478
},
'timezone': 3600,
'id': 2643743,
'name': 'London',
'cod': 200
}
Assuming everything worked well, you should have some similar output. We’re mainly interested in the weather array, main object and the wind object.
The next step in our code is to define a convenience function with the sole purpose of returning false if a key doesn’t exist, and true otherwise. We’ll use it in every variable declaration that relies on fetching information from the JSON, to ensure the program doesn’t crash in the event of a particular piece of data missing. There are also several if-statements that use this function to ensure the core data is present in the JSON data and the program will terminate if it is missing. The
# is json key present
def ijkp(json, key):
try:
buf = json[key]
except KeyError:
return False
return True
if not ijkp(data, "main"):
print("Critical data missing(main). Terminating gracefully")
sys.exit()
if not ijkp(data, "weather"):
print("Critical data missing(weather). Terminating gracefully")
sys.exit()
if not ijkp(data, "wind"):
print("Critical data missing(wind). Terminating gracefully")
sys.exit()
Parsing the Data
Finally, its time to parse the data into a bunch of variables to prepare for output. There are a couple of particulars to note with the following code. Each variable gets a default value of
temp = float(data['main']['temp']) if ijkp(data['main'], "temp") else float(-9999)
feels_like = int(data['main']['feels_like']) if ijkp(data['main'], "feels_like") else None
temp_low = int(data['main']['temp_min']) if ijkp(data['main'], "temp_min") else None
temp_high = int(data['main']['temp_max']) if ijkp(data['main'], "temp_max") else None
humidity = int(data['main']['humidity']) if ijkp(data['main'], "humidity") else None
weather_main = data['weather'][0]['main'] if ijkp(data['weather'][0], "main") else None
weather_desc = data['weather'][0]['description'] if ijkp(data['weather'][0], "description") else None
wind_speed = data['wind']['speed'] if ijkp(data['wind'], "speed") else None
wind_speed_mph = int(wind_speed * 2.237)
dir_index = int(int(data['wind']['deg']) / 22.5) if ijkp(data['wind'], "deg") else None
wind_dir = direction_list[dir_index]
The wind speed is converted to MPH with a magic number. Bad practice, it’s better to declare a constant at the top of the code somewhere but I’m not changing it, as stubborn as I am. Personal projects, right? 😅
The wind direct is calculated using a bit of shady math. We have 16 possible wind directions ranging from North and North-East, all the way to West and North-West. The
Because the wind direction is given in meteorological degrees ranging from 1-360, we take the maximum number and divide that by the number of possible wind directions:
This essentially means that every 22.5 degrees is a new wind direction, and so we divide the given wind direction value by 22.5 to map the wind direction to our
For example, a Northerly wind could be either 0 or 360 degrees (I assume the API would only return 360, but I’m not actually sure).
0 / 22.5 = 0 (the first element of our list, North)
360 / 22.5 = 16 (the last element of our list, North)
180 / 22.5 = 8 (the element storing S, South)
352 / 22.5 = 15.6~, cast to an integer becomes 15 (NNW, North-North-West)
It’s not perfect, we could definitely improve it by rounding to the nearest whole number, but I quite liked the elegance in complexity of this solution and so I never changed it.
Printing The Data
With all that data parsing taken care of, it’s time to print our API request response to the user.
print("")
print("*" * 40)
print("Weather for %s" % location.capitalize())
print("Current time: %s" % datetime.datetime.now().strftime("%H:%M:%S"))
print("*" * 40)
print("")
print("Weather: %s" % weather_desc)
print("Temperature: %d degrees %s" % (temp, temp_unit))
print("Feels like: %s degrees %s" % (feels_like, temp_unit))
print("Wind: %imph in a %s direction" % (wind_speed_mph, wind_dir))
print("Humidity: %i%%" % humidity)
print("")
print("*" * 40)
print("")
We use the old style of python string formatting here to display the information that I wanted to see specifically. The location, time, brief weather description, temperature and feels-like temperature, wind and humidity. Of course, you could choose to display more or less information. That’s the beauty of programming, you can do the same thing many ways and make changes so it suits your own needs!
The Full Code
#! /usr/local/bin/python
import json
import requests
import sys
import datetime
# api-endpoint
URL = "https://api.openweathermap.org/data/2.5/weather"
location = "london"
api_key = "API_KEY"
units = "metric"
temp_unit = "celsius"
PARAMS = {'q': location, 'appid': api_key, 'units': units}
direction_list = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N"]
req = requests.get(url=URL, params=PARAMS)
data = req.json()
# is json key present
def ijkp(json, key):
try:
buf = json[key]
except KeyError:
return False
return True
if not ijkp(data, "main"):
print("Critical data missing(main). Terminating gracefully")
sys.exit()
if not ijkp(data, "weather"):
print("Critical data missing(weather). Terminating gracefully")
sys.exit()
if not ijkp(data, "wind"):
print("Critical data missing(wind). Terminating gracefully")
sys.exit()
temp = float(data['main']['temp']) if ijkp(data['main'], "temp") else float(-9999)
feels_like = int(data['main']['feels_like']) if ijkp(data['main'], "feels_like") else None
temp_low = int(data['main']['temp_min']) if ijkp(data['main'], "temp_min") else None
temp_high = int(data['main']['temp_max']) if ijkp(data['main'], "temp_max") else None
humidity = int(data['main']['humidity']) if ijkp(data['main'], "humidity") else None
weather_main = data['weather'][0]['main'] if ijkp(data['weather'][0], "main") else None
weather_desc = data['weather'][0]['description'] if ijkp(data['weather'][0], "description") else None
wind_speed = data['wind']['speed'] if ijkp(data['wind'], "speed") else None
wind_speed_mph = int(wind_speed * 2.237)
dir_index = int(int(data['wind']['deg']) / 22.5) if ijkp(data['wind'], "deg") else None
wind_dir = direction_list[dir_index]
print("")
print("*" * 40)
print("Weather for %s" % location.capitalize())
print("Current time: %s" % datetime.datetime.now().strftime("%H:%M:%S"))
print("*" * 40)
print("")
print("Weather: %s" % weather_desc)
print("Temperature: %d degrees %s" % (temp, temp_unit))
print("Feels like: %s degrees %s" % (feels_like, temp_unit))
print("Wind: %imph in a %s direction" % (wind_speed_mph, wind_dir))
print("Humidity: %i%%" % humidity)
print("")
print("*" * 40)
print("")
Finally, as I often do, I moved this script to my
The Full Code Updated
While I was writing this post I was looking at the code and thinking “I could make a few good changes here”, so here’s a more updated version of the code. OpenWeather have since upgraded their API to version 3.0 but I’ll be sticking with the 2.5 API because updating code is one thing but switching to a new API version is a whole different world of pain. I’ve removed some unused variables, moved everything to functions and added
#! /usr/local/bin/python
import requests
import sys
import datetime
import argparse
# api-endpoint
URL = "https://api.openweathermap.org/data/2.5/weather"
location = "london"
api_key = "API_KEY"
units = "metric"
temp_unit = "celsius"
KMH_MPH_MX = 2.237
PARAMS = {'q': location, 'appid': api_key, 'units': units}
direction_list = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N"]
# is json key present
def ijkp(json, key):
try:
buf = json[key]
except KeyError:
return False
return True
def prepare_and_output_data(data):
temp = float(data['main']['temp']) if ijkp(data['main'], "temp") else float(-9999)
feels_like = int(data['main']['feels_like']) if ijkp(data['main'], "feels_like") else None
humidity = int(data['main']['humidity']) if ijkp(data['main'], "humidity") else None
weather_desc = data['weather'][0]['description'] if ijkp(data['weather'][0], "description") else None
wind_speed = data['wind']['speed'] if ijkp(data['wind'], "speed") else None
wind_speed_mph = int(wind_speed * KMH_MPH_MX)
dir_index = round(int(data['wind']['deg']) / 22.5) if ijkp(data['wind'], "deg") else None
wind_dir = direction_list[dir_index]
print("")
print("*" * 40)
print(f"Weather for {location.capitalize()}")
print(f"Current time: {datetime.datetime.now().strftime('%H:%M:%S')}")
print("*" * 40)
print(f"Weather: {weather_desc}")
print(f"Temperature: {temp} degrees {temp_unit}")
print(f"Feels like: {feels_like} degrees {temp_unit}")
print(f"Wind: {wind_speed_mph}mph in a {wind_dir} direction")
print(f"Humidity: {humidity}%")
print("*" * 40)
print("")
def parse_args():
parser = argparse.ArgumentParser(description="Python weather")
parser.add_argument("-l", "--location", help="Provide a location")
return parser.parse_args()
def get_data():
req = requests.get(url=URL, params=PARAMS)
data = req.json()
if not ijkp(data, "main") or not ijkp(data, "weather") or not ijkp(data, "wind"):
print("Critical data missing(weather). Terminating gracefully")
sys.exit()
return data
if __name__ == '__main__':
args = parse_args()
if args.location is not None:
location = args.location
data = get_data()
prepare_and_output_data(data)