lib/noaa/observations/station.ex

defmodule NOAA.Observations.Station do
  @moduledoc """
  Fetches the latest observation for a given NOAA station.
  """

  use PersistConfig

  alias NOAA.Observations.{Log, Message, State, TemplatesAgent}

  @typedoc "Station ID"
  @type id :: <<_::32>>
  @typedoc "Station name"
  @type name :: String.t()
  @typedoc "NOAA weather observation"
  @type observation :: map
  @typedoc "Erroneous station"
  @type error :: map
  @typedoc "NOAA station"
  @type t :: {id, name}

  @doc """
  Fetches the latest observation for a given NOAA `station`.

  Returns either tuple `{:ok, observation}` or tuple `{:error, error}`.

  ## Parameters

    - `{station_id, station_name}` - NOAA station
    - `state_code`                 - US state/territory code

  ## Examples

      iex> alias NOAA.Observations.{Station, TemplatesAgent}
      iex> :ok = TemplatesAgent.refresh()
      iex> {:ok, observation} =
      ...>   Station.observation({"KFSO", "KFSO name"}, "VT")
      iex> is_map(observation) and is_binary(observation["wind_mph"])
      true

      iex> alias NOAA.Observations.{Station, TemplatesAgent}
      iex> template =
      ...>   "htp://forecast.weather.gov/xml/current_obs" <>
      ...>     "/display.php?stid=<%=station_id%>"
      iex> TemplatesAgent.update_station_template(template)
      iex> {:error, %{error_text: text, error_code: code, station_id: id}} =
      ...>   Station.observation({"KFSO", "KFSO name"}, "VT")
      iex> {text, code, id}
      {"Non-Existent Domain", :nxdomain, "KFSO"}

      iex> alias NOAA.Observations.{Station, TemplatesAgent}
      iex> template =
      ...>   "https://forecast.weather.gov/xml/past_obs" <>
      ...>     "/display.php?stid=<%=station_id%>"
      iex> TemplatesAgent.update_station_template(template)
      iex> {:error, %{error_text: text, error_code: code, station_id: id}} =
      ...>   Station.observation({"KFSO", "KFSO name"}, "VT")
      iex> {text, code, id}
      {"Not Found", 404, "KFSO"}
  """
  @spec observation(t, State.code()) :: {:ok, observation} | {:error, error}
  def observation({station_id, station_name} = _station, state_code) do
    station_url = TemplatesAgent.station_url(station_id: station_id)

    case HTTPoison.get(station_url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        :ok =
          Log.info(
            :observation_fetched,
            {station_id, station_name, state_code, station_url, __ENV__}
          )

        {
          :ok,
          # <weather>Fog</weather> or <weather> Rain</weather>
          # capture XML tag and value
          ~r{<([^/][^>]+)>\s*(.*?)</\1>}
          # i.e. only subpatterns
          |> Regex.scan(body, capture: :all_but_first)
          # each [tag, value] -> {tag, value}
          |> Map.new(&List.to_tuple/1)
        }

      {:ok, %HTTPoison.Response{status_code: status_code}} ->
        error_text = Message.status(status_code)

        :ok =
          Log.error(
            :observation_not_fetched,
            {station_id, station_name, state_code, status_code, error_text,
             station_url, __ENV__}
          )

        {:error,
         %{
           station_id: station_id,
           station_name: station_name,
           error_code: status_code,
           error_text: error_text,
           station_url: station_url
         }}

      {:error, %HTTPoison.Error{reason: reason}} ->
        error_text = Message.error(reason)

        :ok =
          Log.error(
            :observation_not_fetched,
            {station_id, station_name, state_code, reason, error_text,
             station_url, __ENV__}
          )

        {:error,
         %{
           station_id: station_id,
           station_name: station_name,
           error_code: reason,
           error_text: error_text,
           station_url: station_url
         }}
    end
  end
end