[Ruby] Get weather forecast from OpenWeatherMap with Rails

6 minute read

at first

This is Qitta’s first post! I’m a beginner, so I think there are many points that I can not reach, but I would be glad if you could comment warmly!

Overview

  • Free api [OpenWeatherMap](https://openweathermap.org/api You can get the weather forecast for all parts of Japan by hitting api.
  • Use httpclient for HTTP request and implement it so that it can be hit periodically by using rake task.

Development environment

ruby: 2.7.1 rails: 6.0.3.2

Procedure

  1. Get API KEY
  2. Get the CITY ID of the city you want to get
  3. Creating/saving the City table
  4. Implementation of HTTP request
  5. Creating and saving the WeatherForecast table

1. Get API KEY

Go to the OpenWeatherMap home page and sign in to create an account with Create an account. API KEY will be sent when you activate it from the mail sent.

Save this as an environment variable or credentials. This time I will use credentials. Regarding credentials, this article is very helpful.

EDITOR=vi bin/rails credentials:edit

credentials.yml.enc


open_weahter:
    appid: <API_KEY>
    uri: https://samples.openweathermap.org/data/2.5/forecast

In addition, since I want to acquire the weather every 3 hours this time, I am acquiring the URI to send the request referring to API Document.

It seems that there are many types of weather that you can get even with the free tier! It is interesting to search various API documents.

2. Get CITY ID

Download city.list.json from API documentation.![Screenshot2020-08-0215.54.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684915/9535a511-db9d-f4b3-f042-(028b93b1c498.png)

From this file, get the CITY ID of the city you want to get. This is a partial excerpt. By the way, lon means longitude and lat means latitude.

city.list.json


  {
    "id": 1850147,
    "name": "Tokyo",
    "state": "",
    "country": "JP",
    "coord": {
      "lon": 139.691711,
      "lat": 35.689499
    }
  },

I did a cry cry by hand getting this id… Please note that cities with the same name have different latitudes and longitudes!

If it’s Excel or mac, I think it’s better to list it in numbers etc. and convert it to CSV! By the way, the CSV I created looks like this. I am cutting some columns.

db/csv/cities.csv


Sapporo, 2128295
Aomori, 2130658
Morioka, 2111834
Sendai, 2111149
Akita, 2113126
Yamagata, 2110556
Fukushima, 2112923
Mito, 2111901
Utsunomiya, 1849053
Maebashi, 1857843
Saitama, 6940394
Chiba, 2113015
Tokyo, 1850147
Yokohama, 1848354
Niigata, 1855431
Toyama, 1849876
Kanazawa, 1860243
Fukui, 1863983
Yamanashi, 1848649
Nagano, 1856215
Gifu, 1863640
Shizuoka, 1851715
Nagoya, 1856057
Tsu, 1849796
Otsu, 1853574
Kyoto, 1857910
Osaka, 1853909
Kobe, 1859171
Nara, 1855612
Wakayama, 1926004
Tottori, 1849890
Matsue, 1857550
Okayama, 1854383
Hiroshima, 1862415
Yamaguchi, 1848689
Tokushima, 1850158
Takamatsu, 1851100
Matsuyama, 1926099
Kochi, 1859146
Fukuoka, 1863967
Saga, 1853303
Nagasaki, 1856177
Kumamoto, 1858421
Oita, 1854487
Miyazaki, 1856717
Kagoshima, 1860827
Naha, 1856035

3. Creating/saving the City table

Write the code in seeds.rb or task and save it in the database. This time I implemented it under lib/tasks. CITY ID saves the column name as location_id.

import_csv.rake


  desc'Import cities'
  task cities: [:environment] do
    list = []
    CSV.foreach('db/csv/cities.csv') do |row|
      list << {
        name: row[0],
        location_id: row[1],
      }
    end

    puts'start creating cities'
    begin
      City.create!(list)
      puts'completed!'
    rescue ActiveModel::UnknownAttributeError
      puts'raised error: unknown attributes'
    end
  end

Load the cities.csv created earlier by the CSV.foreach method line by line. You can get the city name in the first column with row[0] and the CITY ID in the second column with row[1], so create a hash array and save it in the database with City.create!. I am.

4. Implementation of HTTP request

Response analysis

Before implementing the HTTP request, first parse the response JSON file.

API document has a detailed description of each item, so please refer to it and get the key of the data you want. A request for one City will be returned in JSON format as below. (Those who use curl command or VScode should try it with REST Clientetc.)

example_resopnse.json


{
  "cod": "200",
  "message": 0,
  "cnt": 40,
  "list": [
    {
      "dt": 1578409200,
      "main": {
        "temp": 284.92,
        "feels_like": 281.38,
        "temp_min": 283.58,
        "temp_max": 284.92,
        "pressure": 1020,
        "sea_level": 1020,
        "grnd_level": 1016,
        "humidity": 90,
        "temp_kf": 1.34
      },
      "weather": [
        {
          "id": 804,
          "main": "Clouds",
          "description": "overcast clouds",
          "icon": "04d"
        }
      ],
      "clouds": {
        "all": 100
      },
      "wind": {
        "speed": 5.19,
        "deg": 211
      },
      "sys": {
        "pod": "d"
      },
      "dt_txt": "2020-01-07 15:00:00"
    },
    

This time, I am creating a table using the following items.

  • list -main -feels_like: Feeling temperature -temp_max: maximum temperature -temp_min: Minimum temperature -weather -id: Weather ID -rain -3h: Precipitation

Only if there is precipitation, rain will be added to the list.

Please refer to here for weather ID. OpenWeatherMap classifies weather by ID. You can also get the weather from the description. However, since there are many types, in this implementation, the weather ID is saved in the database and the method assigns the weather.

HTTP request implementation

First, add httpclient to Gemfile and do a bundle install.

gem'httpclient','~> 2.8','>= 2.8.3'

Next, create lib/api/open_weather_map/request.rb. I thought it wasn’t necessary to go so deep, but I decided to implement such a file arrangement as to implement other classes for this api or implement other api classes.

By default, only the tasks under lib are read, so the following settings are required in config/application.rb. Since it is eager_load_paths, the production environment is also okay.

config/application.rb


config.paths.add'lib', eager_load: true

Hey, it would be great if you could point me out if there is a point like this. The file arrangement was the most troublesome in this implementation…WeatherForecast table to save requests ↓

WeatherForecast  
temp_max float
temp_min float
temp_feel float
weather_id int
rainfall float
date datetime
aquired_at datetime

Below is the Request class implemented.

request.rb


module Api
  module OpenWeatherMap
    class Request
      attr_accessor :query

      def initialize(location_id)
        @query = {
          id: location_id,
          units:'metric',
          appid: Rails.application.credentials.open_weather[:appid],
        }
      end

      def request
        client = HTTPClient.new
        request = client.get(Rails.application.credentials.open_weather[:uri], query) # Return value is every 3 hours 5 days worth of data
        JSON.parse(request.body)
      end

      def self.attributes_for(attrs)
        rainfall = attrs['rain']['3h'] if attrs['rain']
        date = attrs['dt_txt'].in_time_zone('UTC').in_time_zone

        {
          temp_max: attrs['main']['temp_max'],
          temp_min: attrs['main']['temp_min'],
          temp_feel: attrs['main']['feels_like'],
          weather_id: attrs['weather'][0]['id'],
          rainfall: rainfall,
          date: date,
          aquired_at: Time.current,
        }
      end
    end
  end
end

Query string is set in initialize. The query string required for this request includes location_id indicating the CITY ID, API KEY, and units:'metric' to change the temperature display to Celsius display.

We use the attributes_for method as a class method so that the returned request can be saved in the database.

Pay attention to precipitation and forecast dates.

  • Precipitation does not exist if there is no precipitation. Therefore, the condition is branched so that it is acquired only when it is.
  • Be careful about the time zone when it comes to forecast dates. Since the time zone of OpenWeatherMap is UTC, it is saved after being converted to JTC.

Please refer to this article for handling time zones.

5. Creating and saving the WeatherForecast table

Creating a rake task

I want to execute saving/updating to the database regularly, so I will write it in the rake task. That said, I wrote almost the method in the Request class, so I just use it.

open_weather_api.rake


namespace :open_weather_api do
  desc'Requests and save in database'
  task weather_forecasts: :environment do
    City.all.each do |city|
      open_weather = Api::OpenWeatherMap::Request.new(city.location_id)

  #Request limit: 60 times/min
      response = open_weather.request

  # Save 2 days worth of data every 3 hours
      16.times do |i|
        params = Api::OpenWeatherMap::Request.attributes_for(response['list'][i])
        if weather_forecast = WeatherForecast.where(city: city, date: params[:date]).presence
          weather_forecast[0].update!(params)
        else
          city.weather_forecasts.create!(params)
        end
      end
    end
    puts'completed!'
  end
end

This time, the specifications are such that the data every 3 hours is saved and updated in the database every 2 days. The point is the upper limit of the request and whether it is data creation or update.

  • The maximum request is 60 calls/min in Free plan. If you have more than 60 registered cities, you will need to send a separate request. This time it is 47 so there is no problem.
  • The presence method calls the present? method and, if true, returns the receiver itself. If there is already a forecast for the same city at the same time in the database, we call update!, otherwise we call create!.

Finally

If you prepare a weather icon corresponding to the saved weather_id, it will look like a weather forecast! I think it’s better to periodically hit api with cron or heroku schedular for heroku!

It was displayed like this! ![Screenshot 2020-08-02 18.26.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684915/03aea626-f199-5769-2020-(c726255ef229.png)

Thank you for reading the lengthy text!

Reference

https://openweathermap.org/api https://qiita.com/yoshito410kam/items/26c3c6e519d4990ed739