Casey Houser

Home Resume Articles

Using the Open Meteo Weather API in Ruby

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.

How the Open Meteo API Works

API Variables Used in This App

Access Data With Ruby

Full Ruby App

How the Open Meteo API Works

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.

API Variables Used in This App (back to top)

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.

Timezone

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.

Request:
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST
Return:
{"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.

Current Weather

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.

Request:
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST&current=temperature_2m
Return:
{"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.

Hourly Weather

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.

Request:
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST&current=temperature_2m
Return:
{"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:

Request:
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST&current=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.

Celsius or Fahrenheit

You can return temperature values in either celsius (the default) or fahrenheit. Add temperature_unit=fahrenheit to your request.

Request:
https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.00&timezone=EST&current=temperature_2m&temperature_unit=fahrenheit
Return:
{"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}}

Access Data With Ruby (back to top)

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.

Start

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:

  1. Creates a Weather object
  2. Fetches data, overwriting Weather defaults if necessary
  3. Prints data to screen
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
end

start()

Requires

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'

Weather Class

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.

Note the @type

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

Fetch Data From Open Meteo

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

Position, Temp Unit, and Forecast Type

This app includes a number of defaults for its output. In the Weather object:

  1. Position is set to latitude and longitude coordinates for New York City.
  2. Temperature is set to fahrenheit.
  3. Forecast type is set to “current”, meaning it will print the present temperature.

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

Print to Screen

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

Full Ruby App (back to top)

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