Current section
Files
Jump to
Current section
Files
lib/weather.ex
defmodule BCApis.Weather do
@moduledoc """
Weather service for integrating with Open-Meteo API.
Provides functions to fetch current weather data and correlate it with
greenhouse environmental conditions.
Open-Meteo is a free, open-source weather API that doesn't require an API key.
"""
require Logger
@base_url "https://api.open-meteo.com/v1"
@doc """
Fetches current weather data for a given location.
## Parameters
- `lat`: Latitude (float)
- `lon`: Longitude (float)
- `api_key`: Not used (Open-Meteo is free and doesn't require API key)
## Returns
- `{:ok, weather_data}` on success
- `{:error, reason}` on failure
"""
@spec get_current_weather(float(), float(), String.t()) :: {:ok, map()} | {:error, term()}
def get_current_weather(lat, lon, _api_key) do
url = "#{@base_url}/forecast"
params = %{
latitude: lat,
longitude: lon,
current: "temperature_2m,relative_humidity_2m,weather_code,cloud_cover,wind_speed_10m",
timezone: "auto"
}
query_string = URI.encode_query(params)
full_url = "#{url}?#{query_string}"
Logger.info("Weather: Fetching weather data for lat=#{lat}, lon=#{lon}")
case Finch.build(:get, full_url) |> Finch.request(GreenhouseTycoon.Finch) do
{:ok, %Finch.Response{status: 200, body: body}} ->
case Jason.decode(body) do
{:ok, data} ->
Logger.info("Weather: Successfully fetched weather data")
{:ok, normalize_weather_data(data)}
{:error, reason} ->
Logger.error("Weather: Failed to parse JSON response: #{inspect(reason)}")
{:error, {:json_parse_error, reason}}
end
{:ok, %Finch.Response{status: status, body: body}} ->
Logger.error("Weather: API request failed with status #{status}: #{body}")
{:error, {:api_error, status, body}}
{:error, reason} ->
Logger.error("Weather: HTTP request failed: #{inspect(reason)}")
{:error, {:http_error, reason}}
end
end
@doc """
Fetches UV index data for a given location.
## Parameters
- `lat`: Latitude (float)
- `lon`: Longitude (float)
- `api_key`: Not used (Open-Meteo is free and doesn't require API key)
## Returns
- `{:ok, uv_data}` on success
- `{:error, reason}` on failure
"""
@spec get_uv_index(float(), float(), String.t()) :: {:ok, map()} | {:error, term()}
def get_uv_index(lat, lon, _api_key) do
url = "#{@base_url}/forecast"
params = %{
latitude: lat,
longitude: lon,
current: "uv_index",
timezone: "auto"
}
query_string = URI.encode_query(params)
full_url = "#{url}?#{query_string}"
Logger.info("Weather: Fetching UV index for lat=#{lat}, lon=#{lon}")
case Finch.build(:get, full_url) |> Finch.request(GreenhouseTycoon.Finch) do
{:ok, %Finch.Response{status: 200, body: body}} ->
case Jason.decode(body) do
{:ok, data} ->
Logger.info("Weather: Successfully fetched UV index data")
uv_value = get_in(data, ["current", "uv_index"]) || 0
{:ok, %{"value" => uv_value}}
{:error, reason} ->
Logger.error("Weather: Failed to parse UV JSON response: #{inspect(reason)}")
{:error, {:json_parse_error, reason}}
end
{:ok, %Finch.Response{status: status, body: body}} ->
Logger.error("Weather: UV API request failed with status #{status}: #{body}")
{:error, {:api_error, status, body}}
{:error, reason} ->
Logger.error("Weather: UV HTTP request failed: #{inspect(reason)}")
{:error, {:http_error, reason}}
end
end
@doc """
Converts outdoor weather conditions to estimated greenhouse conditions.
Greenhouses typically have:
- Higher temperature (due to greenhouse effect)
- Higher humidity (controlled environment)
- Reduced light (filtered through glass/plastic)
"""
@spec weather_to_greenhouse_conditions(map()) :: map()
def weather_to_greenhouse_conditions(weather_data) do
outdoor_temp = weather_data.temperature
outdoor_humidity = weather_data.humidity
# Estimate greenhouse conditions based on outdoor weather
greenhouse_temp = outdoor_temp + greenhouse_temperature_offset(outdoor_temp)
greenhouse_humidity =
min(outdoor_humidity + greenhouse_humidity_offset(outdoor_humidity), 100)
greenhouse_light = estimate_greenhouse_light(weather_data)
%{
temperature: Float.round(greenhouse_temp, 1),
humidity: Float.round(greenhouse_humidity, 1),
light: Float.round(greenhouse_light, 1)
}
end
# Private functions
defp normalize_weather_data(raw_data) do
current = get_in(raw_data, ["current"]) || %{}
%{
temperature: get_in(current, ["temperature_2m"]) || 0.0,
humidity: get_in(current, ["relative_humidity_2m"]) || 0.0,
# Open-Meteo doesn't provide pressure in current endpoint
pressure: 1013.25,
weather_condition: weather_code_to_condition(get_in(current, ["weather_code"]) || 0),
clouds: get_in(current, ["cloud_cover"]) || 0,
wind_speed: get_in(current, ["wind_speed_10m"]) || 0.0,
# Open-Meteo doesn't provide city name in weather response
city: "Unknown",
# Open-Meteo doesn't provide country in weather response
country: "Unknown"
}
end
defp weather_code_to_condition(0), do: "Clear"
defp weather_code_to_condition(1), do: "Clear"
defp weather_code_to_condition(2), do: "Clouds"
defp weather_code_to_condition(3), do: "Clouds"
defp weather_code_to_condition(45), do: "Fog"
defp weather_code_to_condition(48), do: "Fog"
defp weather_code_to_condition(51), do: "Drizzle"
defp weather_code_to_condition(53), do: "Drizzle"
defp weather_code_to_condition(55), do: "Drizzle"
defp weather_code_to_condition(56), do: "Drizzle"
defp weather_code_to_condition(57), do: "Drizzle"
defp weather_code_to_condition(61), do: "Rain"
defp weather_code_to_condition(63), do: "Rain"
defp weather_code_to_condition(65), do: "Rain"
defp weather_code_to_condition(66), do: "Rain"
defp weather_code_to_condition(67), do: "Rain"
defp weather_code_to_condition(71), do: "Snow"
defp weather_code_to_condition(73), do: "Snow"
defp weather_code_to_condition(75), do: "Snow"
defp weather_code_to_condition(77), do: "Snow"
defp weather_code_to_condition(80), do: "Rain"
defp weather_code_to_condition(81), do: "Rain"
defp weather_code_to_condition(82), do: "Rain"
defp weather_code_to_condition(85), do: "Snow"
defp weather_code_to_condition(86), do: "Snow"
defp weather_code_to_condition(95), do: "Thunderstorm"
defp weather_code_to_condition(96), do: "Thunderstorm"
defp weather_code_to_condition(99), do: "Thunderstorm"
defp weather_code_to_condition(_), do: "Unknown"
defp greenhouse_temperature_offset(outdoor_temp) do
cond do
# Cold weather: significant greenhouse effect
outdoor_temp < 0 -> 8.0
# Cool weather: moderate greenhouse effect
outdoor_temp < 15 -> 5.0
# Mild weather: small greenhouse effect
outdoor_temp < 25 -> 3.0
# Warm weather: minimal greenhouse effect
true -> 1.0
end
end
defp greenhouse_humidity_offset(outdoor_humidity) do
cond do
# Dry air: greenhouse adds moisture
outdoor_humidity < 30 -> 25.0
# Moderate humidity: some increase
outdoor_humidity < 60 -> 15.0
# Already humid: small increase
true -> 10.0
end
end
defp map_condition_to_light("Clear"), do: 85.0
defp map_condition_to_light("Clouds"), do: 60.0
defp map_condition_to_light("Rain"), do: 40.0
defp map_condition_to_light("Drizzle"), do: 45.0
defp map_condition_to_light("Thunderstorm"), do: 25.0
defp map_condition_to_light("Snow"), do: 35.0
defp map_condition_to_light("Mist"), do: 50.0
defp map_condition_to_light("Fog"), do: 30.0
defp map_condition_to_light(_), do: 55.0
defp estimate_greenhouse_light(weather_data) do
cloud_cover = weather_data.clouds
base_light =
map_condition_to_light(weather_data.weather_condition)
# Adjust for cloud cover and greenhouse light transmission (typically 60-70%)
cloud_factor = 1.0 - cloud_cover / 100.0 * 0.6
greenhouse_transmission = 0.65
base_light * cloud_factor * greenhouse_transmission
end
end