The Open Meteo API lets you access weather data for non-commercial use.
In this article, I’ll introduce how the API functions and integrate some of its capabilities into a simple Ruby app you can run in your terminal.
API Variables Used in This App
All calls made to the Open Meteo API work through submissions to its base URL:
https://api.open-meteo.com/v1/forecast
You must always add your location with latitude and longitude (these are for New York City). Grab your data with curl:
curl "https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00"
Open Meteo returns JSON data. Our request for the NYC coordinates returns the following information:
{"latitude"=>40.710335,
"longitude"=>-73.99309,
"generationtime_ms"=>0.00095367431640625,
"utc_offset_seconds"=>0,
"timezone"=>"GMT",
"timezone_abbreviation"=>"GMT",
"elevation"=>7.0}
While there isn’t much here you can use, it does demonstrate the default information Open Meteo will return. You can see your coordinates, the time it took to generate your request, timezone, and elevation if you provide Open Meteo with no other variables.
The Ruby app here uses a handful of additional variables to create a robust return of data it can format into an organized display for the user.
Open Meteo offers hundreds of variables you can use to customize your requests. We’ll use a few basic options here to keep this app simple and let you become familiar with the data types the API returns.
You can see above that this API defaults to the GMT timezone.
We will use the timezone=EST
to match our NYC coordinates, since NYC is in the Eastern time zone. You can replace “EST” here with any time zone abbreviation.
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST
{"latitude"=>40.710335,
"longitude"=>-73.99309,
"generationtime_ms"=>0.00095367431640625,
"utc_offset_seconds"=>-18000,
"timezone"=>"America/New_York",
"timezone_abbreviation"=>"EST",
"elevation"=>7.0}
We’re now given the utc_offset_seconds
key as part of the JSON.
As our own default, the app here will use Open Meteo’s current=temperature_2m
variable to return data about current weather conditions.
The way Open Meteo accepts current
(and in the next section of this article, hourly
) variables is through a comma-separated list of values. For instance, current=temperature_2m,rain,wind_speed_10m
is a valid entry and would tell the API to look for temperature, amount of rain, and wind speed. You can see that the single key can accept multiple values.
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST¤t=temperature_2m
{"latitude"=>40.710335,
"longitude"=>-73.99309,
"generationtime_ms"=>0.01895427703857422,
"utc_offset_seconds"=>-18000,
"timezone"=>"America/New_York",
"timezone_abbreviation"=>"EST",
"elevation"=>7.0,
"current_units"=>{"time"=>"iso8601", "interval"=>"seconds", "temperature_2m"=>"°C"},
"current"=>{"time"=>"2024-11-11T12:30", "interval"=>900, "temperature_2m"=>18.5}}
Now you will find the current_units
and current
keys with values that reflect the current weather.
Keep in mind the default Celsius unit for the temperature and the timestamp format.
If we instead want a forecast, we can request weather in an hourly format. Modify the request URL to use hourly=temperature_2m
.
Similar to the current
weather variable, hourly
accepts a list of comma-separated values. It could look like hourly=temperature_2m,rain,wind_speed_10m
if we wanted additional forecast data.
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST¤t=temperature_2m
{"latitude"=>40.710335,
"longitude"=>-73.99309,
"generationtime_ms"=>0.05900859832763672,
"utc_offset_seconds"=>-18000,
"timezone"=>"America/New_York",
"timezone_abbreviation"=>"EST",
"elevation"=>7.0,
"hourly_units"=>{"time"=>"iso8601", "temperature_2m"=>"°C"},
"hourly"=>
{"time"=>
["2024-11-11T00:00",
"2024-11-11T01:00",
"2024-11-11T02:00",
"2024-11-11T03:00",
"2024-11-11T04:00",
...],
"temperature_2m"=>
[12.6,
13.1,
13.5,
14.0,
14.2,
...
]
}
}
Open Meteo returns a lot more data here from this request. The hourly_units
key replaces the current units from our previous request, and hourly
contains both the time
and temperature
keys.
The values for time
and temperature
are arrays of seven days of forecast temperature data, beginning at 00:00 of the current day.
You can modify the number of forecast days with the start_date
and end_date
variables:
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST¤t=temperature_2m&start_date=2024-11-11&end_date=2024-11-12
Both those variables accept dates with the format YYYY-MM-DD. The above request would return similar data to the original request, but with only two days of temperature data.
You can return temperature values in either celsius (the default) or fahrenheit. Add temperature_unit=fahrenheit
to your request.
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST¤t=temperature_2m&temperature_unit=fahrenheit
{"latitude"=>40.710335,
"longitude"=>-73.99309,
"generationtime_ms"=>0.012993812561035156,
"utc_offset_seconds"=>-18000,
"timezone"=>"America/New_York",
"timezone_abbreviation"=>"EST",
"elevation"=>7.0,
"current_units"=>{"time"=>"iso8601", "interval"=>"seconds", "temperature_2m"=>"°F"},
"current"=>{"time"=>"2024-11-11T13:00", "interval"=>900, "temperature_2m"=>65.9}}
Now that you understand some of the fundamental ways Open Meteo accepts and returns information, you can build an app to print information to screen that’s more useful than plain JSON.
You access the full code for my Weather app below. In this section, I’ll pull out its individual parts to examine how they capture and manipulate the raw Open Meteo data.
The app begins with a brief collecting of command line arguments.
It’s run like ruby weather.rb <-c latitude,longitude> <-u fahrenheit|celsius> <-t hourly|current>
. If it receives any arguments, it will process them; if you run only ruby weather.rb
, the app will return information with defaults for location, temperature type, and forecast type.
You can also ask it for –help
to see usage.
When help is not requested, the app always:
Weather
objectdef start
if ARGV[0] == "--help"
puts "usage: ruby weather.rb [-c <latitude,longitude>] [-u <fahrenheit|celsius>] [-t <hourly|current>]"
elsif ARGV[0] != nil
weather = Weather.new
api_data = fetch_data(ARGV, weather)
print_info(api_data, weather)
else
weather = Weather.new
api_data = fetch_data([], weather)
print_info(api_data, weather)
end
end
start()
Note that the app includes three require
statements. In particular, the net/http
and json
libraries will, respectively, let us request data from Open Meteo and then parse the JSON data it returns.
require 'net/http'
require 'json'
require 'date'
This app uses only a single overarching object for referencing the weather properties.
It creates a Weather
object to hold default values and allow for some easy printing of information to screen.
I’ve built the coordinates and timezone of New York City into this object. It uses those values as defaults when the app is run without user-supplied values.
The date format (YYYY-MM-DD) used here fits into the Open Meteo API requirements.
One stand-out variable here is @type
.
Since this Ruby app, for simplicity, only requests temperature data (which the Open Meteo API accepts as hourly=temperature_2m
in its list of variables), I’ve chosen to include the full key/value pair here instead of piecing it out. This is less flexible, but makes the demonstration here simpler.
A more robust app would address the API’s ability to accept a comma-separated list of hourly
arguments, like hourly=temperature_2m,rain,wind_speed_10m
, by parsing temperature, rain, wind speed, etc. as they appeared.
class Weather
attr_reader :current_date, :current_time, :tomorrow_date
attr_accessor :current_temp,
:forecast_times, :forecast_temps,
:lat, :lon, :unit, :type, :timezone
def initialize
@current_date = (Date.today).strftime('%Y-%m-%e')
@tomorrow_date = (Date.today + 1).strftime('%Y-%m-%e')
@current_time = DateTime.now.strftime('%H:%M')
#defaults for NYC
@lat = 40.71
@lon = -74.00
@timezone = "EST"
@unit = "fahrenheit"
@type = "current=temperature_2m"
end
end
Beginning with the fetch_data
function, Ruby passes around the user-supplied arguments array of ARGV
and the Weather
object the app created.
Like it was mentioned previously, Open Meteo looks for its base URL and a collection of arguments to define the data it should return. Here, fetch_data
splits those halves into the uri
(base) and options
(arguments) and combines them into a single string.
The individual parts of the options
string in this function come from interaction with the get_latlong
, get_unit
, and get_type
functions. Each of those other functions return values that are appended to options
, inserting either user-defined values or the defaults established when Weather
was initialized.
This app uses the net/http
library to get a response
from the Open Meteo server, which it then parses into a JSON object it can further manipulate.
The data
returned here is JSON that Ruby has parsed and can read as keys and values.
If the Open Meteo server returns an error, this program will print that error and exit before finishing any other operations.
def fetch_data(args, weather)
#process command line options, get data from api
#build string of options for API URI
options = "timezone=#{weather.timezone}"
options << get_latlon(args, weather) #latitude, longitude
options << get_unit(args, weather) #get units, C or F
options << get_type(args, weather) #get type, hourly or current
uri = URI("https://api.open-meteo.com/v1/forecast?#{options}")
response = Net::HTTP.get(uri)
data = JSON.parse(response)
#error from server?
if data["error"]
puts data["reason"]
exit
else
return data
end
end
This app includes a number of defaults for its output. In the Weather
object:
The three functions that determine whether or not defaults should be used — get_latlon
, get_unit
, and get_type
— all work in fundamentally the same way.
First, the fetch_data
function calls each one in succession. Each get_…
function asks whether or not its respective command line option and argument exist, and then it processes that information accordingly. I’ll show the get_latlon
function here for brevity. The entire source code is printed below.
get_latlon
looks for the -l
option. If it’s present, it notes the position of that option and looks for the following index in the user-supplied command line argument array (shown here as args
). If text exists in that array index, it tries to parse the supplied information.
The function writes new values to the Weather object if necessary. In any case, it uses the Weather object’s lat
and lon
variables to create a string that will be appended to the base url sent to Open Meteo.
Note that this app is purposefully light on error checking. This is a demonstration that only works through a small portion of the API. All command line input for weather.rb
needs to match the input types the Open Meteo API expects.
def get_latlon(args, weather)
#defaults to LAT, LON constants
#is "-l" option stated?
if args.include?("-l")
idx = args.index("-l") + 1
#is "-l" followed by text?
if (l = args[idx]) != nil
arr = l.split(",", 2)
weather.lat = arr[0]
weather.lon = arr[1]
return "&latitude=#{weather.lat}&longitude=#{weather.lon}"
else
return "&latitude=#{weather.lat}&longitude=#{weather.lon}"
end
else
return "&latitude=#{weather.lat}&longitude=#{weather.lon}"
end
end
Finally, when the fetch_data
function finishes parsing user input and sending a request to the Open Meteo API, this app prints information to the console.
The app looks through the returned JSON from the API for either the current
or hourly
key.
If it finds current
, it prints the date and the current time and temperature.
If it finds hourly
, it also prints the date. Then it loops through the time
key and prints the timestamp and the associated temperature.
def print_info(data, weather) #takes api data (JSON)
#data will be for "current" or "hourly" forecast
if data["current"]
temp = data["current"]["temperature_2m"]
puts "Date: #{weather.current_date}\n\n"
puts "Current time and temp: #{weather.current_time} - #{temp}"
elsif data["hourly"]
#print tomorrow's forecast, times and temps
date_printed = 0
data["hourly"]["time"].each_with_index do |t, idx|
#print date at top
if date_printed == 0
puts "Date: #{weather.tomorrow_date}\n\n"
date_printed = 1
end
dt = DateTime.parse(t)
time = dt.strftime('%H:%M')
temp = data["hourly"]["temperature_2m"][idx]
puts "#{time} - #{temp} "
end
end
end
require 'net/http'
require 'json'
require 'date'
class Weather
attr_reader :current_date, :current_time, :tomorrow_date
attr_accessor :current_temp,
:forecast_times, :forecast_temps,
:lat, :lon, :unit, :type, :timezone
def initialize
@current_date = (Date.today).strftime('%Y-%m-%e')
@tomorrow_date = (Date.today + 1).strftime('%Y-%m-%e')
@current_time = DateTime.now.strftime('%H:%M')
#defaults for NYC
@lat = 40.71
@lon = -74.00
@timezone = "EST"
@unit = "fahrenheit"
@type = "current=temperature_2m"
end
end
def print_info(data, weather) #takes api data (JSON)
#data will be for "current" or "hourly" forecast
if data["current"]
temp = data["current"]["temperature_2m"]
puts "Date: #{weather.current_date}\n\n"
puts "Current time and temp: #{weather.current_time} - #{temp}"
elsif data["hourly"]
#print tomorrow's forecast, times and temps
date_printed = 0
data["hourly"]["time"].each_with_index do |t, idx|
#print date at top
if date_printed == 0
puts "Date: #{weather.tomorrow_date}\n\n"
date_printed = 1
end
dt = DateTime.parse(t)
time = dt.strftime('%H:%M')
temp = data["hourly"]["temperature_2m"][idx]
puts "#{time} - #{temp} "
end
end
end
def fetch_data(args, weather)
#process command line options, get data from api
#build string of options for API URI
options = "timezone=#{weather.timezone}"
options << get_latlon(args, weather) #latitude, longitude
options << get_unit(args, weather) #get units, C or F
options << get_type(args, weather) #get type, hourly or current
uri = URI("https://api.open-meteo.com/v1/forecast?#{options}")
response = Net::HTTP.get(uri)
data = JSON.parse(response)
#error from server?
if data["error"]
puts data["reason"]
exit
else
return data
end
end
def get_latlon(args, weather)
#defaults to LAT, LON constants
#is "-l" option stated?
if args.include?("-l")
idx = args.index("-l") + 1
#is "-l" followed by text?
if (l = args[idx]) != nil
arr = l.split(",", 2)
weather.lat = arr[0]
weather.lon = arr[1]
return "&latitude=#{weather.lat}&longitude=#{weather.lon}"
else
return "&latitude=#{weather.lat}&longitude=#{weather.lon}"
end
else
return "&latitude=#{weather.lat}&longitude=#{weather.lon}"
end
end
def get_unit(args, weather)
#defaults to fahrenheit
#is "-u" option stated?
if args.include?("-u")
idx = args.index("-u") + 1
#is "-u" followed by text?
if args[idx] == "celsius"
weather.unit = "celsius"
return "&temperature_unit=#{weather.unit}"
else
return "&temperature_unit=#{weather.unit}"
end
else
return "&temperature_unit=#{weather.unit}"
end
end
def get_type(args, weather)
#defaults to current
#always limits forecast to 2 days
#is "-t" followed by text?
if args.include?("-t")
idx = args.index("-t") + 1
#is "-t" followed by text?
if args[idx] == "hourly"
weather.type = "hourly=temperature_2m"
return "&#{weather.type}&start_date=#{weather.tomorrow_date}&end_date=#{weather.tomorrow_date}"
else
return "&#{weather.type}"
end
else
return "&#{weather.type}"
end
end
def start
if ARGV[0] == "--help"
puts "usage: ruby weather.rb [-c <latitude,longitude>] [-u <fahrenheit|celsius>] [-t <hourly|current>]"
elsif ARGV[0] != nil
weather = Weather.new
api_data = fetch_data(ARGV, weather)
print_info(api_data, weather)
else
weather = Weather.new
api_data = fetch_data([], weather)
print_info(api_data, weather)
end
rescue => e
puts "Program error: #{e}"
end
start()