defmodule Yeelight.Discover do
@moduledoc """
start the discovery server and get the device list
```
Yeelight.Discover.start()
:timer.sleep(1000)
devices = Yeelight.Discover.devices()
```
"""
use GenServer
defmodule State do
@moduledoc false
defstruct udp: nil, devices: [], handlers: [], port: nil
end
@port 1982
@multicast_group {239, 255, 255, 250}
@discover_message """
M-SEARCH * HTTP/1.1\r\n
HOST: 239.255.255.250:1982\r\n
MAN: "ssdp:discover"\r\n
ST: wifi_bulb\r\n
"""
def start_link do
GenServer.start_link(__MODULE__, @port, name: __MODULE__)
end
def start do
start_link()
GenServer.call(__MODULE__, :start)
end
def discover, do: GenServer.call(__MODULE__, :discover)
def devices, do: GenServer.call(__MODULE__, :devices)
@impl true
def init(port) do
{:ok, %State{:port => port}}
end
@impl true
def handle_call(:start, _from, state) do
{:reply, :ok,
case state.udp do
nil ->
udp_options = [
:binary,
add_membership: {@multicast_group, {0, 0, 0, 0}},
multicast_if: {0, 0, 0, 0},
multicast_loop: false,
multicast_ttl: 2,
reuseaddr: true
]
{:ok, udp} = :gen_udp.open(state.port, udp_options)
Process.send_after(self(), :discover, 0)
Map.update(state, :udp, udp, fn _ -> udp end)
_exists ->
state
end}
end
def handle_call(:devices, _from, state) do
{:reply, state.devices, state}
end
def handle_call(:send, _from, state) do
:gen_udp.send(state.udp, @multicast_group, @port, discover())
{:noreply, state}
end
@impl true
def handle_info(:discover, state) do
Process.send_after(self(), {:send, @discover_message}, (:rand.uniform() * 1000) |> round)
Process.send_after(self(), :discover, 61_000)
{:noreply, state}
end
def handle_info({:send, discover}, state) do
:gen_udp.send(state.udp, @multicast_group, @port, discover)
{:noreply, state}
end
def handle_info(<<@discover_message, _::binary>>, state) do
{:noreply, state}
end
def handle_info({:udp, _s, _ip, _port, <<"HTTP/1.1 200 OK\r\n", device::binary>>}, state) do
{:noreply, device |> parse_device |> update_devices(state)}
end
def handle_info({:udp, _s, _ip, _port, <<"NOTIFY * HTTP/1.1\r\n", device::binary>>}, state) do
{:noreply, device |> parse_device |> update_devices(state)}
end
def parse_device(body) do
map =
body
|> String.split(["\r\n", "\n"], trim: true)
|> Enum.reduce(%{}, fn x, acc ->
case String.split(x, ": ", parts: 2) do
[k, v] when k in ~w(bright color_mode ct rgb hue sat) ->
Map.put(acc, String.to_atom(k), String.to_integer(v))
[k, v] when k in ~w(id model fw_ver power name) ->
Map.put(acc, String.to_atom(k), v)
["support", v] ->
Map.put(acc, :support, String.split(v, " ", trim: true))
["Location", location] ->
uri =
location
|> String.replace("yeelight", "http")
|> URI.parse()
acc
|> Map.put(:host, uri.host)
|> Map.put(:port, uri.port)
[_k, _v] ->
acc
end
end)
struct!(Yeelight.Device, map)
end
def update_devices(device, state) when is_map(device) do
case state.devices |> Enum.any?(&(&1.id == device.id)) do
false -> Map.update(state, :devices, state.devices, &[device | &1])
true -> state
end
end
end