4.9 KiB · text History 9c0fc16
defmodule LambentExWeb.DevicesChannel do
  @moduledoc """
  Mirrors `LambentExWeb.DevicesLive.Index`: the discovered device map, plus
  rename / place / order / poke / probe.

  Pushes
    * `"devices"`  — `%{devices: %{mac => device}}`, a snapshot on join and a
      delta on every publish. Merge them by mac, the way the liveview does.
    * `"probe_result"` — `%{ip: ip, status: "lambent" | "not_lambent" | "no_iface", sox: n}`
    * `"interfaces"` — `[%{name:, addr:, enabled:}]`, which interfaces the
      sweep runs on, on join and whenever one is turned off or back on.
  """

  use Phoenix.Channel
  require Logger

  alias LambentEx.Scan.ESP8266x7777, as: S
  alias LambentExWeb.Channels.Payload

  @pubsub LambentEx.PubSub

  @impl true
  def join("devices", _params, socket) do
    Phoenix.PubSub.subscribe(@pubsub, "scan-82667777")
    Phoenix.PubSub.subscribe(@pubsub, "scan-manual")
    Phoenix.PubSub.subscribe(@pubsub, "scan-ifaces")
    send(self(), :snapshot)
    {:ok, assign(socket, :devices, %{})}
  end

  # mqtt devices only exist in the publishing process, so the snapshot covers
  # the 7777 ones and the rest land on the next tick
  @impl true
  def handle_info(:snapshot, socket) do
    devices = S.tagged_devices()
    push(socket, "devices", %{devices: Payload.devices(devices)})
    push(socket, "interfaces", %{interfaces: LambentEx.Scan.NMAP.interfaces()})
    {:noreply, assign(socket, :devices, devices)}
  end

  @impl true
  def handle_info({:interfaces, interfaces}, socket) do
    push(socket, "interfaces", %{interfaces: interfaces})
    {:noreply, socket}
  end

  @impl true
  def handle_info({:devices_pub, devices}, socket) do
    push(socket, "devices", %{devices: Payload.devices(devices)})
    {:noreply, assign(socket, :devices, Map.merge(socket.assigns.devices, devices))}
  end

  @impl true
  def handle_info({:manual_result, ip, status}, socket) do
    payload =
      case status do
        {:lambent, sox} -> %{ip: ip, status: "lambent", sox: max(sox, 1)}
        other -> %{ip: ip, status: to_string(other)}
      end

    push(socket, "probe_result", payload)
    {:noreply, socket}
  end

  @impl true
  def handle_in("rename", %{"id" => id} = params, socket) do
    name = params |> Map.get("name") |> blank_to_nil()

    case type_of(socket, id) do
      t when t in ["7777-8266", "7777-flex"] -> S.rename(:mac, id, name)
      "mqtt-tasmota" -> LambentEx.MQTT.rename(dname(socket, id), id, name)
      other -> unhandled("rename", other)
    end

    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("replace", %{"id" => id} = params, socket) do
    place = params |> Map.get("place") |> blank_to_nil()

    case type_of(socket, id) do
      t when t in ["7777-8266", "7777-flex"] -> S.replace(:mac, id, place)
      "mqtt-tasmota" -> LambentEx.MQTT.replace(dname(socket, id), id, place)
      other -> unhandled("replace", other)
    end

    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("reorder", %{"id" => id, "ord" => ord}, socket) do
    case type_of(socket, id) do
      t when t in ["7777-8266", "7777-flex"] -> S.reorder(:mac, id, ord)
      other -> unhandled("reorder", other)
    end

    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("poke", %{"id" => id} = params, socket) do
    case type_of(socket, id) do
      t when t in ["7777-8266", "7777-flex"] -> S.poke(id, poke_mode(params))
      "mqtt-tasmota" -> LambentEx.MQTT.do_poke(dname(socket, id), id)
      other -> unhandled("poke", other)
    end

    {:reply, :ok, socket}
  end

  # beyond the prefixes the scanner filters out on its own, an interface can be
  # told to stay out of the sweep
  @impl true
  def handle_in("interface", %{"name" => name, "enabled" => enabled}, socket)
      when is_boolean(enabled) do
    LambentEx.Scan.NMAP.set_enabled(name, enabled)
    {:reply, :ok, socket}
  end

  # ask everything we know about what it is running now, rather than waiting
  # for the sweep to come round
  @impl true
  def handle_in("refresh", _params, socket) do
    S.refresh()
    {:reply, :ok, socket}
  end

  @impl true
  def handle_in("probe", %{"ip" => ip}, socket) do
    ip = String.trim(ip)

    case IP.from_string(ip) do
      {:ok, addr} when tuple_size(addr) == 4 ->
        S.manual(ip)
        {:reply, :ok, socket}

      _otherwise ->
        {:reply, {:error, %{reason: "#{ip} is not a valid ipv4 address"}}, socket}
    end
  end

  defp poke_mode(params) do
    case params |> Map.get("mode") do
      "indicator" -> :indicator
      "strands" -> :strands
      _otherwise -> :auto
    end
  end

  defp type_of(socket, id) do
    socket.assigns.devices |> Map.get(id, %{}) |> Map.get("type")
  end

  defp dname(socket, id) do
    socket.assigns.devices |> Map.get(id, %{}) |> Map.get("dname")
  end

  defp blank_to_nil(""), do: nil
  defp blank_to_nil(other), do: other

  defp unhandled(cmd, type) do
    Logger.info("Need handler for #{cmd} on this type: #{type}")
    :ok
  end
end