README.md

# <img src="priv/static/images/logo.png" alt="Logo" height="30"/> Weather

[![Build
Status](https://github.com/spencerolson/weather/actions/workflows/elixir.yml/badge.svg)](https://github.com/spencerolson/weather/actions/workflows/elixir.yml) [![License](https://img.shields.io/hexpm/l/weather.svg)](https://github.com/spencerolson/weather/blob/main/LICENSE.md) [![Hex.pm](https://img.shields.io/hexpm/v/weather.svg)](https://hex.pm/packages/weather) [![Documentation](https://img.shields.io/badge/documentation-purple)](https://hexdocs.pm/weather)

An Elixir library for fetching data from the [OpenWeatherMap One Call API 3.0](https://openweathermap.org/api/one-call-3) service.

Use it as a dependency in your project:

```elixir
Mix.install([
  {:weather, "~> 0.3.1"}
])

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

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

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

 🌧️ 12PM - 2PM, 3PM - 5PM

 🌞 11:01AM | 🌚 12:52AM

 66°  ⬇   65°  ⬆   67°  ⬆   73°  ⬇   71°
 12PM     3PM      6PM      9PM      12AM

 66° | moderate rain | 92% humidity

# => :ok
```

or standalone as a command line interface:

```bash
$ 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
- 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
- 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"](https://openweathermap.org/api/one-call-3) 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](#options) section below).

### Creating an API Key

Follow the directions on OpenWeatherMap's ["One Call API 3.0"](https://openweathermap.org/api/one-call-3) 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](https://home.openweathermap.org/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:

```elixir
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`:

```elixir
def deps do
  [{:weather, "~> 0.3.1"}]
end
```

and you're ready to go!

```elixir
# 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](#options) for the list of options you can pass to [`Weather.Opts.new/1`].

All available modules can be found on [hexdocs](https://hexdocs.pm/weather).

### Using `Weather` as a Commmand Line Interface

1. Clone the repository
   ```bash
   $ git clone https://github.com/spencerolson/weather.git
   ```
2. Generate the executable and move it to a directory that is in your `PATH`
   ```bash
   $ cd weather
   $ mix deps.get
   $ mix escript.build
   $ mv weather ~/bin/ # or some other directory that is in your PATH
   ```
3. Use it from anywhere!

   If you've created an OpenWeatherMap API Key:
   ```bash
   $ 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:
   ```bash
   $ 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:
   ```bash
   $ 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:
   ```bash
   $ 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:

```bash
                    << 🌧️ 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](https://hexdocs.pm/elixir/keywords-and-maps.html#keyword-lists) 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)
- `--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)
- `--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. (string)

## Customization

### How to Customize your Weather Report

1. Define a module in `lib/weather/report/custom` that implements the `Weather.Report` behaviour (definines `generate/1`).

   ```elixir
   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`

   ```elixir
   config :weather, custom_reports: [Weather.Report.Custom.FullMoon]
   ```

3. Re-generate the escript

   ```bash
   $ mix escript.build
   ```

4. Wait for a full moon...🌑🌒🌓🌔🌕

5. Enjoy your customized weather report

   ```bash
   $ ./weather

   🌝🌚 OMG FULL MOON TONIGHT 🌚🌝

   🌞 5:17AM | 🌚 8:25PM

   76°  ⬇   74°  ⬇   64°  ⬇   60°  ⬇   58°
   3PM      6PM      9PM      12AM     3AM

   77° | scattered clouds | 37% humidity
   ```

6. Issue a [pull request](https://github.com/spencerolson/weather/pulls) to have your custom report added to the repo so others can use it! :D (please don't include the changes you made to `config/config.exs` in your PR)

## 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
](#optional-set-environment-variables-for-your-api-key-latitude-and-longitude))

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

```bash
$ weather --zip 60618

Chicago

🌞 6:07AM | 🌚 7:39PM

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

79° | broken clouds | 44% humidity

```

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 Celcius and using 24-hour time.

```bash
$ 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

```bash
$ 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

```bash
$ 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

```bash
$ 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)

```elixir
opts = Weather.Opts.new(zip: 60618)
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).

```elixir
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.

[`Weather.Opts.new/1`]: https://hexdocs.pm/weather/Weather.Opts.html#new/1
[`Weather.Report.RainMinutely.generate/1`]: https://hexdocs.pm/weather/Weather.Report.RainMinutely.html#generate/1