lib/vintage_net_wizard.ex

defmodule VintageNetWizard do
  @moduledoc """
  Documentation for VintageNetWizard.
  """

  alias VintageNetWizard.{APMode, BackendServer, Web.Endpoint}

  @type stop_reason() :: :shutdown | :timeout

  @doc """
  Run the wizard.

  This means the WiFi module will be put into access point mode and the web
  server will be started.

  Options:

    * `:backend` - Implementation for communicating with the network drivers (defaults to `VintageNetWizard.Backend.Default`)
    * `:captive_portal` - Whether to run in captive portal mode (defaults to `true`)
    * `:device_info` - A list of string tuples to render in a table in the footer (see `README.md`)
    * `:ifname` - The network interface to use (defaults to `"wlan0"`)
    * `:ap_ifname` - The network interface to use to run the endpoint. Defaults to the value of `ifname`.
    * `:inactivity_timeout` - Minutes to run before automatically stopping (defaults to 10 minutes) or `:infinity` to disable the timeout
    * `:on_exit` - `{module, function, args}` tuple specifying callback to perform after stopping the server.
    * `:ssl` - A Keyword list of `:ssl.tls_server_options`. See `Plug.SSL.configure/1`.
    * `:ui` - a subset of UI configuration for title, title color, and button color.
  """
  @spec run_wizard([Endpoint.opt()]) :: :ok | {:error, String.t()}
  def run_wizard(opts \\ []) do
    ifname = Keyword.get(opts, :ifname, "wlan0")
    ap_ifname = Keyword.get(opts, :ap_ifname, ifname)
    configurations = get_network_configs(ifname)

    opts =
      opts
      |> Keyword.put(:configurations, configurations)
      |> Keyword.put(:ifname, ifname)
      |> Keyword.put(:ap_ifname, ap_ifname)

    with :ok <- APMode.into_ap_mode(ap_ifname),
         :ok <- Endpoint.start_server(opts),
         :ok <- BackendServer.start_scan() do
      :ok
    else
      # Already running is still ok
      {:error, :already_started} -> :ok
      error -> error
    end
  end

  @doc """
  Conditionally run the wizard if there is no configurations present

  This function is the same `VintageNetWizard.run_wizard/1` however it will
  first check if there are any configurations for the interface.

  This is useful if you want a device to start the wizard only if there are no
  configurations for the interface. When there are configurations found for the
  interface this function returns `:configured` to let the consuming application
  know that the wizard wasn't needed.

  If you want more control on how to start the wizard or if you want to force
  start the wizard you can call `VintageNetWizard.run_wizard/1`.
  """
  @spec run_if_unconfigured([Endpoint.opt()]) :: :ok | :configured | {:error, String.t()}
  def run_if_unconfigured(opts \\ []) do
    ifname = Keyword.get(opts, :ifname, "wlan0")

    if wifi_configured?(ifname) do
      :configured
    else
      run_wizard(opts)
    end
  end

  @doc """
  Stop the wizard.

  This will apply the current configuration in memory and completely
  stop the web and backend processes.
  """
  @spec stop_wizard(stop_reason()) :: :ok | {:error, String.t()}
  def stop_wizard(stop_reason \\ :shutdown) do
    with :ok <- BackendServer.complete(),
         :ok <- Endpoint.stop_server(stop_reason) do
      :ok
    else
      error ->
        error
    end
  end

  @doc """
  Check if an interface has a configuration
  """
  @spec wifi_configured?(VintageNet.ifname()) :: boolean()
  def wifi_configured?(ifname) do
    VintageNet.get(["interface", ifname, "config"])
    |> get_in([:vintage_net_wifi, :networks])
    |> has_networks?()
  end

  defp has_networks?(nil), do: false
  defp has_networks?([]), do: false
  defp has_networks?(_networks), do: true

  defp get_network_configs(ifname) do
    config = VintageNet.get(["interface", ifname, "config"])

    case get_in(config, [:vintage_net_wifi, :networks]) do
      nil -> []
      networks -> networks
    end
  end
end