Skip to content

An Elixir library and command-line interface for fetching weather data from the OpenWeatherMap API.

License

Notifications You must be signed in to change notification settings

spencerolson/weather

Repository files navigation

Logo Weather

Build Status License Hex.pm Documentation

Run in Livebook

An Elixir library for fetching data from the OpenWeatherMap One Call API 3.0 service.

Use it as a dependency in your project:

Mix.install(
  [{:weather, "~> 0.3.8"}]
)

opts = Weather.Opts.new(test: "rain")
Weather.API.fetch_weather(opts)
# =>
# {:ok,
#  %Req.Response{
#    status: 200,
#    headers: %{},
#    body: %{
#      "current" => %{...},
#      "daily" => %{...},
#      "hourly" => %{...},
#      "minutely" => %{...},
#      ...
#    }
#  }
# }

Weather.get!(opts) |> IO.puts

                   << 🌧️  7:28AM - 8:27AM >>

[                                                    ....    ]
[                                              ............  ]
[                                    ........................]
[                                   .........................]
[                                 ...........................]
[                                ............................]
                +              +              +

🌧️  8AM - 9AM, 10AM - 12PM

🌞 6:01AM | 🌚 7:52PM

66°  ⬇   65°  ⬆   67°  ⬆   73°  ⬇   71°
7AM      10AM     1PM      4PM      7PM

66° | moderate rain | 92% humidity

# => :ok

or standalone as a command line interface:

$ weather --units metric --no-twelve

🌞 06:08 | 🌚 19:42

24°  ⬇   23°  ⬇   18°  ⬇   16°  ⬇   15°
16       19       22       01       04

25° | clear sky | 48% humidity

Features

  • Access to raw API responses
  • Access to formatted rain reports
  • Customizable ANSI-colorized output
  • Detailed rain intensity forecast for the next hour
  • Weather alerts
  • Customizable length and interval for hourly weather
  • Customizable time (12 or 24 hour) and temp display (celsius, fahrenheit, kelvin)
  • Usable as a dependency or standalone CLI
  • Weather lookup by ZIP code and country code
  • Mock weather responses to test responses for varying conditions

Getting Started

In order to fetch real weather data, you'll want to first create an API Key for OpenWeatherMap's "One Call API 3.0" service. However, if you're not ready to do that yet, you can always play around with Weather by passing the test option (for more information, see the Options section below).

Creating an API Key

Follow the directions on OpenWeatherMap's "One Call API 3.0" page for creating an API Key. Up to 1,000 calls per day are provided for free.

After creating your API Key, make sure to set your "Calls per day (no more than)" to 1,000 at your subscriptions page. This ensures you'll never go over the limit of the 1,000 free API calls per day (I believe the default is 2,000 which is a bit irritating).

It can take some time for your API key to become ready for use. I think it took a few hours for my key to become activated.

(Optional) Set environment variables for your API Key, Latitude, and Longitude

Set the OPENWEATHER_API_KEY, WEATHER_LATITUDE, and WEATHER_LONGITUDE environment variables. With these set, you won't need to pass api_key, latitude, or longitude to Weather.Opts.new/1 and it'll default to using these values.

For example, with these environment variables set you can simply:

Weather.get!() |> IO.puts()

🌞 6:09AM | 🌚 7:40PM

76°  ⬆   77°  ⮕   77°  ⬇   68°  ⬇   64°
12PM     3PM      6PM      9PM      12AM

76° | clear sky | 51% humidity

# => :ok

Using Weather as a Dependency

Add weather to your list of dependencies in mix.exs

def deps do
  [{:weather, "~> 0.3.8"}]
end

and you're ready to go!

# If you haven't created an OpenWeatherMap API Key yet, this can be:
# opts = Weather.Opts.new(test: "rain")
#
# If you've set the environment variables for your API key, latitude, and longitude,
# this can be:
# opts = Weather.Opts.new()
opts = Weather.Opts.new(
  api_key: "your-openweather-api-key",
  latitude: 41.411835,
  longitude: -75.665245
)
{:ok, response} = Weather.API.fetch_weather(opts)
# =>
# {:ok,
#  %Req.Response{
#    status: 200,
#    headers: %{},
#    body: %{
#      "current" => %{...},
#      "daily" => %{...},
#      "hourly" => %{...},
#      "minutely" => %{...},
#      ...
#    }
#  }
# }

{sun_report, _, _} = Weather.Report.SunriseSunset.generate({[], response.body, opts})
IO.puts(sun_report)
🌞 6:20AM | 🌚 7:50PM
# => :ok

See Options for the list of options you can pass to Weather.Opts.new/1.

All available modules can be found on hexdocs.

Using Weather as a Command Line Interface

  1. Install the escript

    $ mix escript.install hex weather
  2. Add the escript to your $PATH (more info here)

  3. Use it from anywhere!

    If you've created an OpenWeatherMap API Key:

    $ weather --api-key your-openweather-api-key --latitude 41.411835 --longitude -75.665245
    
    🌞 6:20AM | 🌚 7:50PM
    
    77°  ⮕   77°  ⬇   69°  ⬇   59°  ⬇   57°
    1PM      4PM      7PM      10PM     1AM
    
    77° | clear sky | 51% humidity
    

    If you haven't created an OpenWeatherMap API Key yet:

    $ weather --test clear
    
    🌞 5:17AM | 🌚 8:25PM
    
    76°  ⬇   74°  ⬇   64°  ⬇   60°  ⬇   58°
    3PM      6PM      9PM      12AM     3AM
    
    77° | scattered clouds | 37% humidity
    

    If you've set the environment variables for your API key, latitude, and longitude:

    $ weather
    
    🌞 6:20AM | 🌚 7:50PM
    
    77°  ⮕   77°  ⬇   69°  ⬇   59°  ⬇   57°
    1PM      4PM      7PM      10PM     1AM
    
    77° | clear sky | 51% humidity

    For a list of all available options, see:

    $ weather --help
    
    <big list of options>

"Minutely" Rain Chart

When any rain is expected within the next hour, a rain chart will be output by Weather.Report.RainMinutely.generate/1. It looks something like:

                    << 🌧️ 12:28PM - 1:27PM >>

 [                                                            ]
 [                                                            ]
 [                    ........................................]
 [............................................................]
 [............................................................]
 [............................................................]
                 +              +              +

For each column, the number of dots corresponds with the rain intensity for that minute:

  • 0 dots = "No Rain"
  • 1 dot = "Very Light" (< 0.25 mm/hr)
  • 2 dots = "Light" (>= 0.25 and < 1 mm/hr)
  • 3 dots = "Moderate" (>= 1 and < 4 mm/hr)
  • 4 dots = "Heavy" (>= 4 and < 16 mm/hr)
  • 5 dots = "Very Heavy" (>= 16 and < 50 mm/hr)
  • 6 dots = "Violent" (>= 50 mm/hr)

The + characters are 15-minute markers. So the first + is 15 minutes from now, the second + is 30 minutes from now, and the third + is 45 minutes from now.

Options

Option names listed below are for the command line interface. All options can also be passed as a Keyword list to Weather.Opts.new/1. Hyphens must be converted to underscores for the option names passed to Weather.Opts.new/1. For example, Weather.Opts.new(hide_alerts: true)

Boolean switches take no values. --someval sets the value to true and --no-someval sets the value to false.

  • --help (-h): Prints the help message. (boolean)
  • --hide-alerts (-l): Hides weather alerts, even when alerts are available. Default is false, which shows alerts if there are any available. (boolean)
  • --feels-like (-f): Shows the 'feels like' temperature instead of the actual temperature. Defaults to false. (boolean)
  • --alert-titles-only (-o): Shows only the titles of weather alerts. Default is false, which shows titles along with full alert descriptions. (boolean)
  • --colors (-c): Enables colorized output for the hourly report. Defaults to true. (boolean)
  • --color-codes (-d): A comma-separated list of up to 10 color codes to use for colorized output. Values must be integers in the range of 0 to 255 inclusive, or '_' to use the default. Defaults to 202,214,226,148,39,51,15,245,88,9. Values correspond with the following categories: arctic, freezing, cold, chilly, cool, mild, warm, hot, very_hot, scorching. (string)
  • --every (-e): Sets the hour interval at which data is reported for the hourly report. Defaults to 3. (integer)
  • --hours (-r): Sets the number of hours to report on for the hourly report. Defaults to 12. Max is 48. (integer)
  • --label (-b): The name of the location for which weather data is being fetched. If present, the report will include - the label in the output. If not provided but a zip code is provided, the label will be set to the name of the location associated with the zip code. (string)
  • --latitude (-t): The latitude of the location for which to fetch weather data. (float)
  • --longitude (-n): The longitude of the location for which to fetch weather data. (float)
  • --api-key (-a): The OpenWeatherMap API key. (string)
  • --units (-u): The units in which to return the weather data. Options: "metric" (celsius), "celsius", "imperial" (fahrenheit), "fahrenheit", "standard" (kelvin), "kelvin". (string)
  • --test (-s): Fake weather data for testing purposes. Options: "clear", "rain", "storm". (string)
  • --twelve (-w): Enables 12-hour time format for the hourly report. Defaults to true. When false, 24-hour time format is used. (boolean)
  • --zip (-z): A zip code string to fetch weather data for. This can be used in place of latitude and longitude. For most accurate results format should be zip/post code and ISO 3166 country code divided by comma. See https://openweathermap.org/api/geocoding-api#direct_zip for more information. (string)

Customization

Custom Colors

Weather.Colors.list()
# => prints available colors and their associated codes

pink = 201
gray = 248
Weather.Opts.new(color_codes: %{cool: pink, warm: gray})

or as a command line interface:

$ weather --color-codes "_,_,_,_,201,_,248,_,_,_"

Adding a Custom Report

  1. Define a module that implements the Weather.Report behaviour (defines generate/1).

    defmodule Weather.Report.Custom.FullMoon do
      @moduledoc """
      A custom `Weather.Report` that prints when there's a full moon.
      """
    
      use Weather.Report
    
      @full_moon_phase 0.5
    
      @doc """
      Generates a full moon report.
      """
      @impl Weather.Report
      def generate({report, %{"daily" => [%{"moon_phase" => @full_moon_phase} | _]} = body, opts}) do
        {
          ["🌝🌚 OMG FULL MOON TONIGHT 🌚🌝" | report],
          body,
          opts
        }
      end
    
      def generate(weather), do: weather
    end
  2. Add that module to the list of custom reports in your config/config.exs

    config :weather, custom_reports: [Weather.Report.Custom.FullMoon]
  3. Wait for a full moon...🌑🌒🌓🌔🌕

  4. Enjoy your customized weather report

    Weather.get!() |> IO.puts()
    
    🌝🌚 OMG FULL MOON TONIGHT 🌚🌝
    
    🌞 5:17AM | 🌚 8:25PM
    
    76°  ⬇   74°  ⬇   64°  ⬇   60°  ⬇   58°
    3PM      6PM      9PM      12AM     3AM
    
    77° | scattered clouds | 37% humidity
    
    :ok
  5. If you're feeling generous, issue a pull request to have your custom report added to the repo so others can use it! :D Please place the report in lib/weather/report/custom/. See lib/weather/report/custom/full_moon.ex for an example.

Examples

All examples below assume the api key, latitude, and longitude environment variables have been set (see (Optional) Set environment variables for your API Key, Latitude, and Longitude )

Fetch weather using a ZIP code (no latitude or longitude needed)

$ weather --zip E14,GB --no-twelve --units metric

London

🌞 06:22 | 🌚 19:33

19°  ⬆   21°  ⬇   20°  ⬇   17°  ⮕   17°
13       16       19       22       01

19° | clear sky | 76% humidity

YELLOW RAIN WARNING (Sat 21:00 - Sun 18:00)
Whilst there remains some uncertainty with exact details, areas of heavy and at times thundery rain are expected to spread north, then west, across England and Wales from this evening and overnight. These areas of heavy rain may become more persistent across western areas during Sunday daytime whilst slow-moving heavy showers and thunderstorms are likely to develop further east.

Fetching weather by ZIP code will result in two API calls to OpenWeather; one to get the location data for that ZIP, and one to get the weather.

Fetch weather with results in Celsius and using 24-hour time.

$ weather --units celsius --no-twelve

🌞 06:07 | 🌚 19:39

26°  ⬆   27°  ⮕   27°  ⬇   23°  ⬇   21°
12       15       18       21       00

26° | broken clouds | 44% humidity

Fetch weather for every hour for the next 5 hours

$ weather --every 1 --hours 5

🌞 6:07AM | 🌚 7:39PM

79°  ⮕   79°  ⬆   80°  ⬆   81°  ⬆   82°  ⮕   82°
12PM     1PM      2PM      3PM      4PM      5PM

79° | broken clouds | 44% humidity

Use fake storm data, showing only titles (hiding descriptions) for alerts

$ weather --test storm --alert-titles-only

                   << 🌧️ 8:28PM - 9:27PM >>

[                                                            ]
[.........       ...............     ........................]
[............................................................]
[............................................................]
[............................................................]
[............................................................]
                +              +              +

🌧️ 9PM - 2AM

🌞 5:44AM | 🌚 8:42PM

77°  ⬇   73°  ⬇   70°  ⬇   68°  ⬆   72°
8PM      11PM     2AM      5AM      8AM

77° | very heavy rain | 76% humidity

FLOOD WATCH (Tue 5:00PM - Wed 7:00AM)

TORNADO WATCH (Tue 7:48PM - Tue 9:00PM)

SEVERE THUNDERSTORM WARNING (Tue 8:12PM - Tue 9:30PM)

SEVERE THUNDERSTORM WARNING (Tue 7:42PM - Tue 8:45PM)

SEVERE THUNDERSTORM WARNING (Tue 8:17PM - Tue 8:45PM)

Fetch weather, adding a label and removing ANSI colors from the output

$ weather --label "Home Sweet Home" --no-colors

Home Sweet Home

🌞 6:07AM | 🌚 7:39PM

80°  ⬆   81°  ⮕   81°  ⬇   74°  ⬇   69°
12PM     3PM      6PM      9PM      12AM

80° | broken clouds | 43% humidity

Get the latitude, longitude, and name of a location by ZIP code (iex only)

opts = Weather.Opts.new(zip: "60618,US")
opts.latitude
# => 41.9464
opts.longitude
# => -87.7042
opts.label
# => "Chicago"

View the temperature thresholds for colorized output (iex only)

When viewed in iex, you will see different colors for each temp in the output (you can't see them in this README because markdown doesn't support ANSI colors).

Weather.Colors.list_current()

Current Color Configuration (temps in fahrenheit)

-10°
0°
33°
40°
50°
60°
70°
80°
90°
100°
# => :ok

License

MIT License

Copyright (c) 2024 Spencer Olson

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

An Elixir library and command-line interface for fetching weather data from the OpenWeatherMap API.

Resources

License

Stars

Watchers

Forks

Languages