# ExOwm
[](https://hex.pm/packages/ex_owm)
[](https://hexdocs.pm/ex_owm/)
[](https://github.com/kociamber/ex_owm/blob/master/LICENSE)
[](https://hex.pm/packages/ex_owm)
[](https://github.com/kociamber/ex_owm/commits/master)
**Modern, fast [OpenWeatherMap](http://openweathermap.org/technology) API client for Elixir applications.**
## Features
Clean, intuitive API with validated location types
Concurrent requests with automatic batching
Smart caching with configurable TTL per endpoint
Telemetry integration for production observability
Modern dependencies (Req, Nebulex)
Comprehensive error handling with detailed feedback
## Installation
Add ExOwm as a dependency to your `mix.exs` file:
```elixir
defp deps() do
[
{:ex_owm, "~> 2.0"}
]
end
```
## Upgrading from 1.x?
See [UPGRADE.md](UPGRADE.md) for a comprehensive migration guide. The v1.x API still works with deprecation warnings.
## Configuration
To use OWM APIs, you need to [register](https://home.openweathermap.org/users/sign_up) for an account (free plan is available) and obtain an API key.
Add the following configuration to your `config/config.exs` file:
```elixir
config :ex_owm, api_key: System.get_env("OWM_API_KEY")
# Optional: Disable caching (enabled by default)
config :ex_owm, :cache_enabled, false
# Optional: Configure cache TTL per endpoint (when cache is enabled)
config :ex_owm, :default_ttl, :timer.minutes(10)
config :ex_owm, :current_weather_ttl, :timer.minutes(5)
config :ex_owm, :forecast_5day_ttl, :timer.hours(1)
```
## Quick Start
### Basic Usage
```elixir
# Using validated location constructors (recommended)
location = ExOwm.Location.by_city("Warsaw")
{:ok, weather} = ExOwm.current_weather(location, units: :metric)
# Using raw maps (backward compatible)
{:ok, weather} = ExOwm.current_weather(%{city: "Warsaw"}, units: :metric)
# Multiple location types
location = ExOwm.Location.by_city("London", country: "uk")
location = ExOwm.Location.by_coords(52.374031, 4.88969)
location = ExOwm.Location.by_id(2759794)
location = ExOwm.Location.by_zip("94040", country: "us")
```
### Batch Requests
```elixir
locations = [
ExOwm.Location.by_city("Warsaw"),
ExOwm.Location.by_city("London", country: "uk"),
ExOwm.Location.by_coords(52.374031, 4.88969)
]
results = ExOwm.current_weather_batch(locations, units: :metric, lang: :pl)
# => [
# {:ok, %{"name" => "Warszawa", ...}},
# {:ok, %{"name" => "London", ...}},
# {:ok, %{"name" => "Amsterdam", ...}}
# ]
```
### Error Handling
```elixir
location = ExOwm.Location.by_city("InvalidCity")
case ExOwm.current_weather(location) do
{:ok, data} ->
IO.inspect(data)
{:error, :not_found, details} ->
IO.puts("City not found: #{inspect(details)}")
{:error, :api_key_invalid, _} ->
IO.puts("Invalid API key")
{:error, reason} ->
IO.puts("Request failed: #{inspect(reason)}")
end
```
## Available Endpoints
ExOwm supports the following OpenWeatherMap [APIs](http://openweathermap.org/api):
### Weather Data
#### Current Weather
```elixir
# Single location
ExOwm.current_weather(location, units: :metric)
# Batch
ExOwm.current_weather_batch(locations, units: :metric)
```
#### One Call API
Includes current weather, minute/hourly/daily forecasts, and historical data in one call.
```elixir
# Requires coordinates
location = ExOwm.Location.by_coords(52.374031, 4.88969)
ExOwm.one_call(location, units: :metric)
# Batch
ExOwm.one_call_batch(locations, units: :metric)
```
#### 5 Day / 3 Hour Forecast
```elixir
ExOwm.forecast_5day(location, units: :metric)
ExOwm.forecast_5day_batch(locations, units: :metric)
```
#### 4 Day Hourly Forecast
**Note:** Requires paid OpenWeatherMap plan.
```elixir
ExOwm.forecast_hourly(location, units: :metric)
ExOwm.forecast_hourly_batch(locations, units: :metric)
```
#### 1-16 Day Daily Forecast
```elixir
ExOwm.forecast_16day(location, cnt: 16, units: :metric)
ExOwm.forecast_16day_batch(locations, cnt: 16, units: :metric)
```
#### Historical Weather Data
Available for the last 5 days.
```elixir
yesterday = System.system_time(:second) - 86400
location =
ExOwm.Location.by_coords(52.374031, 4.88969)
|> ExOwm.Location.with_timestamp(yesterday)
ExOwm.historical(location, units: :metric)
```
### Air Quality
#### Current Air Pollution
Get current air quality index and pollutant concentrations (CO, NO, NO2, O3, SO2, NH3, PM2.5, PM10).
```elixir
location = ExOwm.Location.by_coords(52.374031, 4.88969)
{:ok, data} = ExOwm.air_pollution(location)
# AQI scale: 1 = Good, 2 = Fair, 3 = Moderate, 4 = Poor, 5 = Very Poor
aqi = get_in(data, ["list", Access.at(0), "main", "aqi"])
ExOwm.air_pollution_batch(locations)
```
#### Air Pollution Forecast
Four-day hourly forecast of air quality.
```elixir
ExOwm.air_pollution_forecast(location)
ExOwm.air_pollution_forecast_batch(locations)
```
#### Historical Air Pollution
Historical air quality data from November 27, 2020 onwards.
```elixir
now = System.system_time(:second)
yesterday = now - 86_400
ExOwm.air_pollution_history(location, start: yesterday, end: now)
ExOwm.air_pollution_history_batch(locations, start: yesterday, end: now)
```
### Geocoding
#### Direct Geocoding
Convert location names to coordinates.
```elixir
{:ok, locations} = ExOwm.geocode("London", limit: 5)
{:ok, locations} = ExOwm.geocode("London,GB", limit: 1)
# Returns: [%{"name" => "London", "lat" => 51.5074, "lon" => -0.1278, "country" => "GB"}]
```
#### Reverse Geocoding
Convert coordinates to location names.
```elixir
{:ok, locations} = ExOwm.reverse_geocode(52.374031, 4.88969, limit: 1)
# Returns: [%{"name" => "Amsterdam", "lat" => 52.374031, "lon" => 4.88969, "country" => "NL"}]
```
## Request Options
Weather APIs:
`:units` - `:metric`, `:imperial`, or `:standard` (default Kelvin)
`:lang` - Language code as atom (`:pl`, `:en`, `:ru`, `:de`)
`:cnt` - Number of forecast days for 16-day forecast (1-16)
`:ttl` - Cache TTL in milliseconds (overrides default)
Geocoding APIs:
`:limit` - Max results (default 5)
Air pollution history:
`:start` - Start timestamp (Unix time, UTC)
`:end` - End timestamp (Unix time, UTC)
## Telemetry
ExOwm emits telemetry events for observability:
```elixir
# In your application.ex
:telemetry.attach_many(
"ex-owm-handler",
[
[:ex_owm, :request, :start],
[:ex_owm, :request, :stop]
],
&MyApp.Telemetry.handle_owm_event/4,
nil
)
defmodule MyApp.Telemetry do
require Logger
def handle_owm_event([:ex_owm, :request, :stop], measurements, metadata, _) do
duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info("OpenWeatherMap API call completed",
endpoint: metadata.endpoint,
cache: metadata.cache,
duration_ms: duration_ms
)
end
def handle_owm_event([:ex_owm, :request, :start], _, _, _), do: :ok
end
```
### Event Metadata
**`[:ex_owm, :request, :start]`:**
- `endpoint` - API endpoint (`:current_weather`, `:one_call`, etc.)
- `location` - Location struct or map
- `cache` - `:hit` or `:miss`
**`[:ex_owm, :request, :stop]`:**
- All metadata from `:start` event
- `result` - The API result tuple
- `duration` - Request duration (native time units in measurements)
## Caching
ExOwm includes optional built-in caching using [Nebulex](https://github.com/cabol/nebulex). Caching is enabled by default to reduce API costs and improve response times.
**Disable caching:**
```elixir
config :ex_owm, :cache_enabled, false
```
**Default TTL by endpoint:**
- `current_weather`: 10 minutes
- `one_call`: 10 minutes
- `forecast_5day`: 1 hour
- `forecast_hourly`: 1 hour
- `forecast_16day`: 6 hours
- `historical`: 24 hours
**Override in config:**
```elixir
config :ex_owm, :default_ttl, :timer.minutes(10)
config :ex_owm, :current_weather_ttl, :timer.minutes(5)
```
**Override per-request:**
```elixir
ExOwm.current_weather(location, ttl: :timer.minutes(1))
```
When caching is disabled, all requests go directly to the OpenWeatherMap API without any caching layer.
## Location Validation
Use `ExOwm.Location` constructors for early validation:
```elixir
# City - validates non-empty string
location = ExOwm.Location.by_city("Warsaw")
location = ExOwm.Location.by_city("London", country: "uk")
# Coordinates - validates lat/lon ranges
location = ExOwm.Location.by_coords(52.374031, 4.88969)
# Raises ArgumentError if lat not in [-90, 90] or lon not in [-180, 180]
# City ID - validates positive integer
location = ExOwm.Location.by_id(2759794)
# ZIP code - requires country parameter
location = ExOwm.Location.by_zip("94040", country: "us")
# Add timestamp for historical queries
location =
ExOwm.Location.by_coords(52.37, 4.89)
|> ExOwm.Location.with_timestamp(1615546800)
```
## Running Tests
Tests require a valid OpenWeatherMap API key and are disabled by default:
```bash
# Set your API key
export OWM_API_KEY="your_key_here"
# Remove the :api_based_test exclusion in test/test_helper.exs
# Then run tests
mix test
```
## Architecture
ExOwm v2.0 uses a simplified architecture:
- **No coordinator GenServers** - Direct function calls with `Task.async_stream` for batching
- **Smart caching** - Nebulex with Shards backend
- **Modern HTTP** - Req client with built-in retry/telemetry support
- **Telemetry events** - Full observability for production monitoring
This results in **~400 fewer lines** of code compared to v1.x while being more maintainable and performant.
## API Limitations
Please note that with a standard (free) license/API key, you may be limited in:
- Number of requests per minute (60 calls/minute for free tier)
- Access to certain endpoints (hourly forecast, 16-day forecast)
Refer to OpenWeatherMap [pricing plans](http://openweathermap.org/price) for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is MIT licensed. Please see the [`LICENSE.md`](https://github.com/Kociamber/ex_owm/blob/master/LICENSE.md) file for more details.