Modern, fast OpenWeatherMap API client for Elixir applications.
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
Add ExOwm as a dependency to your mix.exs file:
defp deps() do
[
{:ex_owm, "~> 2.0"}
]
endSee UPGRADE.md for a comprehensive migration guide. The v1.x API still works with deprecation warnings.
To use OWM APIs, you need to register for an account (free plan is available) and obtain an API key.
Add the following configuration to your config/config.exs file:
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)# 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")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", ...}}
# ]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)}")
endExOwm supports the following OpenWeatherMap APIs:
# Single location
ExOwm.current_weather(location, units: :metric)
# Batch
ExOwm.current_weather_batch(locations, units: :metric)Includes current weather, minute/hourly/daily forecasts, and historical data in one call.
# 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)ExOwm.forecast_5day(location, units: :metric)
ExOwm.forecast_5day_batch(locations, units: :metric)Note: Requires paid OpenWeatherMap plan.
ExOwm.forecast_hourly(location, units: :metric)
ExOwm.forecast_hourly_batch(locations, units: :metric)ExOwm.forecast_16day(location, cnt: 16, units: :metric)
ExOwm.forecast_16day_batch(locations, cnt: 16, units: :metric)Available for the last 5 days.
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)Get current air quality index and pollutant concentrations (CO, NO, NO2, O3, SO2, NH3, PM2.5, PM10).
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)Four-day hourly forecast of air quality.
ExOwm.air_pollution_forecast(location)
ExOwm.air_pollution_forecast_batch(locations)Historical air quality data from November 27, 2020 onwards.
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)Convert location names to coordinates.
{: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"}]Convert coordinates to location names.
{:ok, locations} = ExOwm.reverse_geocode(52.374031, 4.88969, limit: 1)
# Returns: [%{"name" => "Amsterdam", "lat" => 52.374031, "lon" => 4.88969, "country" => "NL"}]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)
ExOwm emits telemetry events for observability:
# 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[:ex_owm, :request, :start]:
endpoint- API endpoint (:current_weather,:one_call, etc.)location- Location struct or mapcache-:hitor:miss
[:ex_owm, :request, :stop]:
- All metadata from
:startevent result- The API result tupleduration- Request duration (native time units in measurements)
ExOwm includes optional built-in caching using Nebulex. Caching is enabled by default to reduce API costs and improve response times.
Disable caching:
config :ex_owm, :cache_enabled, falseDefault TTL by endpoint:
current_weather: 10 minutesone_call: 10 minutesforecast_5day: 1 hourforecast_hourly: 1 hourforecast_16day: 6 hourshistorical: 24 hours
Override in config:
config :ex_owm, :default_ttl, :timer.minutes(10)
config :ex_owm, :current_weather_ttl, :timer.minutes(5)Override per-request:
ExOwm.current_weather(location, ttl: :timer.minutes(1))When caching is disabled, all requests go directly to the OpenWeatherMap API without any caching layer.
Use ExOwm.Location constructors for early validation:
# 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)Tests require a valid OpenWeatherMap API key and are disabled by default:
# 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 testExOwm v2.0 uses a simplified architecture:
- No coordinator GenServers - Direct function calls with
Task.async_streamfor batching - Smart caching - Nebulex with Shards backend
- Modern HTTP - Req client with built-in retry/telemetry support
- Telemetry events - Observability for production monitoring
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 for details.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is MIT licensed. Please see the LICENSE.md file for more details.