lib/dht.ex

defmodule DHT do
  @moduledoc File.read!("README.md")
             |> String.split(~r/<!-- .*DOC !-->/)
             |> Enum.drop(1)
             |> Enum.join("\n")

  @type period :: non_neg_integer()
  @type pin :: non_neg_integer()
  @type reading :: %{temperature: float(), humidity: float()}
  @type sensor :: :am2302 | :dht11 | :dht22 | 11 | 22

  @doc ~s(
    Start polling of readings at specified period intervals that are delivered as telemtry events
    #{
         File.read!("README.md")
         |> String.split(~r/<!-- POLLDOC !-->/)
         |> Enum.drop(1)
         |> hd()
       }
  )
  @spec start_polling(pin(), sensor(), period()) ::
          DynamicSupervisor.on_start_child() | {:error, %ArgumentError{}}
  def start_polling(pin, sensor, period \\ 2)

  def start_polling(pin, sensor, period) when is_integer(period) and period >= 2 do
    case sanitize_args(pin, sensor) do
      {:ok, pin, sensor} ->
        DHT.Telemetry.start_polling(pin, sensor, period)

      err ->
        err
    end
  end

  def start_polling(_pin, _sensor, _period) do
    {:error, %ArgumentError{message: "time period must be >= 2 seconds"}}
  end

  @doc ~s(
  Take a reading on the specified pin for a sensor type
  #{
         File.read!("README.md")
         |> String.split("<!-- READDOC !-->")
         |> Enum.drop(1)
         |> hd()
       }
  )
  @spec read(pin(), sensor()) :: {:ok, reading} | {:error, %ArgumentError{}} | {:error, integer()}
  def read(pin, sensor) do
    case sanitize_args(pin, sensor) do
      {:ok, pin, sensor} -> DHT.Port.read(pin, sensor)
      err -> err
    end
  end

  @spec stop_polling(pin(), sensor()) :: :ok | {:error, :not_found}
  def stop_polling(pin, sensor) do
    case sanitize_args(pin, sensor) do
      {:ok, pin, sensor} -> DHT.Telemetry.stop_polling(pin, sensor)
      err -> err
    end
  end

  defp sanitize_args(pin, sensor) do
    with {:ok, pin} <- sanitize_pin(pin),
         {:ok, sensor} <- sanitize_sensor(sensor) do
      {:ok, pin, sensor}
    else
      {:error, msg} -> {:error, %ArgumentError{message: msg}}
    end
  end

  defp sanitize_pin(pin) when is_integer(pin), do: {:ok, pin}
  defp sanitize_pin(pin), do: {:error, "invalid pin: #{inspect(pin)}"}

  @supported_sensors Enum.reduce([{:dht11, 11}, {:dht22, 22}, {:am2302, 22}], [], fn {k, v},
                                                                                     acc ->
                       [{to_string(k), v}, {to_string(v), v} | acc]
                     end)
                     |> Map.new()

  defp sanitize_sensor(sensor) do
    cleansed = String.downcase(to_string(sensor))

    case Map.get(@supported_sensors, cleansed) do
      nil -> {:error, "invalid sensor: #{inspect(sensor)}"}
      sensor_num -> {:ok, sensor_num}
    end
  end
end