Current section
Files
Jump to
Current section
Files
lib/elixir_weather_data_genserver.ex
defmodule ElixirWeatherData.GenServer do
use GenServer
@valid_languages ["en", "de"]
@doc """
Starts the gen server.
"""
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
@doc """
Checks the given options.
"""
def init(_opts) do
opts = [
Application.get_env(:elixir_weather_data, :api)[:key],
Application.get_env(:elixir_weather_data, :api)[:language],
Enum.into(Application.get_env(:elixir_weather_data, :api)[:coordinates], %{})
]
case check_opts(opts) do
[] -> {:ok, get_data(opts)}
error_reasons -> {:stop, error_reasons}
end
end
def handle_call(:get, _from, {:error, parameters_map, _error_reason}) do
create_reply(extract_request_parameters({:error, parameters_map}))
end
def handle_call(:get, _from, old_state = {:ok, _data}) do
case cached_data_should_be_updated?(old_state) do
true -> create_reply(old_state, extract_request_parameters(old_state))
_ -> create_reply(old_state)
end
end
defp create_reply({:ok, data}) when is_map(data) do
{:reply, {:ok, Map.delete(data, :parameters)}, {:ok, data}}
end
defp create_reply(old_state, parameters) when is_tuple(old_state) and is_list(parameters) do
case get_data(parameters) do
{:ok, data} -> create_reply({:ok, data})
{:error, _parameters_map, _error_reason} -> create_reply(old_state)
end
end
defp create_reply(parameters) when is_list(parameters) do
case get_data(parameters) do
{:ok, data} -> create_reply({:ok, data})
{:error, parameters_map, error_reason} -> {:reply, {:error, error_reason}, {:error, parameters_map, error_reason}}
end
end
defp check_opts([api_key, language, coordinates]) do
errors = check_api_key(api_key)
errors = [check_language(language) | errors]
errors = [check_coordinates(coordinates) | errors]
List.flatten(errors)
end
defp check_api_key(api_key) when is_binary(api_key) do
case String.match?(api_key, ~r/[a-z0-9]{32}/) do
true -> []
_ -> ["invalid api key given"]
end
end
defp check_api_key(_api_key), do: ["no api key given"]
defp check_language(language) when is_binary(language) do
case Enum.member?(@valid_languages, language) do
true -> []
_ -> ["invalid language given"]
end
end
defp check_language(_language), do: ["no language given"]
defp check_coordinates(%{lat: lat, lon: lon}) when is_float(lat) and is_float(lon) do
[]
end
defp check_coordinates(_coordinates), do: ["no or invalid geo coordinates given"]
@doc """
Returns the current state:
* `{:ok, weather_data_map}`
* `{:error, error_reason_string}`
"""
def get do
GenServer.call __MODULE__, :get#, 30_000
end
defp get_module(:prod), do: ElixirWeatherData.OpenWeatherMapApi.HttpClient
defp get_module(:dev) do
case Application.get_env(:elixir_weather_data, :dev)[:mode] do
nil -> ElixirWeatherData.OpenWeatherMapApi.Sandbox
:sandbox -> ElixirWeatherData.OpenWeatherMapApi.Sandbox
:http_client -> ElixirWeatherData.OpenWeatherMapApi.HttpClient
end
end
defp get_module(:test), do: ElixirWeatherData.OpenWeatherMapApi.InMemory
defp get_data(parameters = [api_key, language, coordinates]) do
lat = round_value(coordinates.lat, 2)
lon = round_value(coordinates.lon, 2)
"http://api.openweathermap.org/data/2.5/weather?lat=#{lat}&lon=#{lon}&lang=#{language}&appid=#{api_key}"
|> get_module(Mix.env).send_request()
|> parse()
|> create_data_map()
|> add_request_parameters(parameters)
end
defp cached_data_should_be_updated?({:error, _reason}), do: true
defp cached_data_should_be_updated?({:ok, data}) do
one_hour_in_seconds = 60*60
data[:created_at] < (timestamp_now() - one_hour_in_seconds)
end
defp parse({:ok, body}), do: Poison.decode(body)
defp parse({:error, reason}), do: {:error, reason}
defp create_data_map({:error, reason}), do: {:error, reason}
defp create_data_map({:ok, data}) do
centigrade =
data["main"]["temp"]
|> kelvin_to_centigrade()
|> round_value
humidity = data["main"]["humidity"]
pressure =
data["main"]["pressure"]
|> round_value
fahrenheit =
centigrade
|> centigrade_to_fahrenheit
|> round_value
[%{"description" => description} | _tail] = data["weather"]
[%{"icon" => icon} | _tail] = data["weather"]
wind_in_meters_per_second =
data["wind"]["speed"]
|> round_value
wind_direction_in_degrees =
data["wind"]["deg"]
|> round_value
wind_in_kilometers_per_hour =
meters_per_second_to_kilometers_per_hour(wind_in_meters_per_second)
|> round_value
{:ok, %{created_at: timestamp_now(), centigrade: centigrade, fahrenheit: fahrenheit, weather: description, wind_in_kilometers_per_hour: wind_in_kilometers_per_hour, wind_in_meters_per_second: wind_in_meters_per_second, humidity_in_percent: humidity, pressure_in_hectopascal: pressure, icon: icon, icon_url: "http://openweathermap.org/img/w/#{icon}.png", wind_direction_in_degrees: wind_direction_in_degrees, wind_direction_abbreviation: wind_direction_abbreviation(wind_direction_in_degrees)}}
end
defp add_request_parameters({:ok, data}, parameters) do
{:ok, Map.put(data, :parameters, parameters)}
end
defp add_request_parameters({:error, error_reason}, parameters) do
{:error, %{parameters: parameters}, error_reason}
end
defp extract_request_parameters({_, %{parameters: parameters}}) do
parameters
end
defp round_value(value, precision \\ 0)
defp round_value(nil, _), do: nil
defp round_value(value, 0) do
round(value)
end
defp round_value(value, precision) when is_float(value) do
Float.round(value, precision)
end
defp round_value(value, _precision), do: value
defp kelvin_to_centigrade(value) do
value - 273.15
end
defp centigrade_to_fahrenheit(value) do
((value * 9) / 5) + 32
end
defp meters_per_second_to_kilometers_per_hour(value) do
value * 3.6 #0.868976242 #1.609
end
defp timestamp_now do
Timex.local
|> Timex.to_unix()
end
defp wind_direction_abbreviation(nil), do: ""
defp wind_direction_abbreviation(degrees) do
directions = %{
[348.75, 360] => "N",
[0, 11.25] => "N",
[11.25, 33.75] => "NNE",
[33.75, 56.25] => "NE",
[56.25, 78.75] => "ENE",
[78.75, 101.25] => "E",
[101.25, 123.75] => "ESE",
[123.75, 146.25] => "SE",
[146.25, 168.75] => "SSE",
[168.75, 191.25] => "S",
[191.25, 213.75] => "SSW",
[213.75, 236.25] => "SW",
[236.25, 258.75] => "WSW",
[258.75, 281.25] => "W",
[281.25, 303.75] => "WNW",
[303.75, 326.25] => "NW",
[326.25, 348.75] => "NNW"
}
{_key, direction_abbravation} = Enum.find(directions, fn{[first, last], _value} -> first <= degrees && degrees <= last end)
direction_abbravation
end
end