Packages
grizzly
5.0.2
9.1.4
9.1.2
9.1.1
9.1.0
9.0.0
8.15.3
8.15.2
8.15.1
8.15.0
8.14.0
8.13.0
8.12.0
8.11.3
8.11.2
8.11.1
8.11.0
8.10.0
8.9.0
8.8.1
8.8.0
8.7.1
8.7.0
8.6.12
8.6.11
8.6.10
8.6.9
8.6.8
8.6.7
retired
8.6.6
8.6.5
8.6.4
8.6.3
8.6.2
8.6.1
8.6.0
8.5.3
8.5.2
8.5.1
8.5.0
8.4.0
8.3.0
8.2.3
8.2.2
8.2.1
8.2.0
8.1.0
8.0.1
8.0.0
7.4.3
7.4.2
7.4.1
7.4.0
7.3.0
7.2.0
7.1.4
7.1.3
7.1.2
7.1.1
7.1.0
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.8.8
6.8.7
6.8.6
6.8.5
6.8.4
6.8.3
6.8.2
6.8.1
6.8.0
6.7.1
6.7.0
6.6.1
6.6.0
6.5.1
6.5.0
6.4.0
6.3.0
6.2.0
6.1.1
6.1.0
6.0.1
6.0.0
5.4.1
5.4.0
5.3.0
5.2.8
5.2.7
5.2.6
5.2.5
5.2.4
5.2.3
5.2.2
5.2.1
5.2.0
5.1.2
5.1.1
5.1.0
5.0.2
5.0.1
5.0.0
4.0.1
4.0.0
3.0.0
2.1.0
2.0.0
1.0.1
1.0.0
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.9.0-rc.4
0.9.0-rc.3
0.9.0-rc.2
0.9.0-rc.1
0.9.0-rc.0
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
Elixir Z-Wave library
Current section
Files
Jump to
Current section
Files
lib/grizzly/connections/async_connection.ex
defmodule Grizzly.Connections.AsyncConnection do
@moduledoc false
# A connection type that is useful for doing long running operations that are
# allowed to be canceled, or if the operation may need to request more
# information from a user.
# don't use this connection type unless it is for a special reason. Normally,
# you will want to wrap this connection in a GenServer as normally there is
# some long running state tied to needing one of these connection types.
use GenServer
alias Grizzly.{Connections, Connection, Options, Report, Transport, ZIPGateway, ZWave}
alias Grizzly.Commands.CommandRunner
alias Grizzly.Connections.{KeepAlive, CommandList}
alias Grizzly.ZWave.Command
alias Grizzly.ZWave.Commands.ZIPPacket
require Logger
defmodule State do
@moduledoc false
defstruct transport: nil,
socket: nil,
commands: CommandList.empty(),
owner: nil,
keep_alive: nil,
node_id: nil
end
def child_spec(node_id, opts \\ []) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [node_id, opts]}, restart: :transient}
end
@spec start_link(Options.t(), ZWave.node_id(), [Connection.opt()]) :: GenServer.on_start()
def start_link(grizzly_options, node_id, opts \\ []) do
name = Connections.make_name({:async, node_id})
opts = Keyword.put_new(opts, :owner, self())
GenServer.start_link(__MODULE__, [grizzly_options, node_id, opts], name: name)
end
@spec send_command(Grizzly.node_id(), Command.t(), keyword()) :: {:ok, reference()}
def send_command(node_id, command, opts \\ []) do
name = Connections.make_name({:async, node_id})
GenServer.call(name, {:send_command, command, opts}, 140_000)
end
@spec stop_command(Grizzly.node_id(), reference()) :: :ok
def stop_command(node_id, command_ref) do
name = Connections.make_name({:async, node_id})
GenServer.call(name, {:stop_command, command_ref})
end
@spec command_alive?(Grizzly.node_id(), reference()) :: boolean()
def command_alive?(node_id, command_ref) do
name = Connections.make_name({:async, node_id})
GenServer.call(name, {:command_alive?, command_ref})
end
def stop(node_id) do
# TODO close socket
name = Connections.make_name({:async, node_id})
GenServer.stop(name, :normal)
end
@impl GenServer
def init([grizzly_options, node_id, opts]) do
host = ZIPGateway.host_for_node(node_id, grizzly_options)
transport_impl = grizzly_options.transport
transport_opts = [
ip_address: host,
port: grizzly_options.zipgateway_port
]
case Transport.open(transport_impl, transport_opts) do
{:ok, transport} ->
{:ok,
%State{
transport: transport,
keep_alive: KeepAlive.init(node_id, 25_000),
owner: Keyword.fetch!(opts, :owner),
node_id: node_id
}}
{:error, reason} ->
{:stop, reason}
end
end
@impl GenServer
def handle_call({:send_command, command, send_opts}, {waiter, _ref}, state) do
{:ok, command_runner, command_ref, new_command_list} =
CommandList.create(state.commands, command, state.node_id, waiter, send_opts)
case do_send_command(command_runner, state) do
:ok ->
{:reply, {:ok, command_ref},
%State{
state
| commands: new_command_list,
keep_alive: KeepAlive.timer_restart(state.keep_alive)
}}
end
end
def handle_call({:stop_command, command_ref}, _from, state) do
{:ok, new_commands} = CommandList.stop_command_by_ref(state.commands, command_ref)
{:reply, :ok, %State{state | commands: new_commands}}
end
def handle_call({:command_alive?, command_ref}, _from, state) do
{:reply, CommandList.has_command_ref?(state.commands, command_ref), state}
end
@impl GenServer
def handle_info(:keep_alive_tick, state) do
%State{keep_alive: keep_alive} = state
new_keep_alive =
keep_alive
|> KeepAlive.make_command()
|> KeepAlive.run(&do_send_command(&1, state))
{:noreply, %State{state | keep_alive: new_keep_alive}}
end
# handle when there is a timeout and command runner stops
def handle_info(
{:grizzly, :command_timeout, command_runner_pid, grizzly_command},
state
) do
if grizzly_command.source.name == :keep_alive do
{:noreply, state}
else
waiter = CommandList.get_waiter_for_runner(state.commands, command_runner_pid)
do_timeout_reply(waiter, grizzly_command)
{:noreply,
%State{
state
| commands: CommandList.drop_command_runner(state.commands, command_runner_pid)
}}
end
end
def handle_info(data, state) do
%State{transport: transport, node_id: node_id} = state
case Transport.parse_response(transport, data) do
{:ok, :connection_closed} ->
Logger.debug("[Grizzly] connection to node #{inspect(node_id)} closed")
{:stop, :normal, state}
{:ok, transport_response} ->
updated_state = handle_commands(transport_response.command, state)
{:noreply, updated_state}
{:error, error} ->
error_message = Exception.message(error)
Logger.warn("[Grizzly] #{inspect(error_message)}")
{:noreply, state}
end
end
defp handle_commands(%Command{name: :keep_alive}, state) do
%State{state | keep_alive: KeepAlive.timer_restart(state.keep_alive)}
end
defp handle_commands(zip_packet, state) do
updated_state =
case CommandList.response_for_zip_packet(state.commands, zip_packet) do
{:retry, command_runner, new_command_list} ->
:ok = do_send_command(command_runner, state)
%State{state | commands: new_command_list}
{:continue, new_command_list} ->
if !ZIPPacket.ack_response?(zip_packet) do
# Since we are doing async communications we need to handle when the
# connection gets an unhandled command from the Z-Wave
send(
state.owner,
{:grizzly, :report, to_report(zip_packet, state.node_id)}
)
end
%State{state | commands: new_command_list}
{waiter, {:error, :nack_response, new_command_list}} ->
send(waiter, {:error, :nack_response})
%State{state | commands: new_command_list}
{waiter, {%Report{} = report, new_command_list}} ->
send(waiter, {:grizzly, :report, report})
%State{state | commands: new_command_list}
end
%State{updated_state | keep_alive: KeepAlive.timer_restart(state.keep_alive)}
end
defp to_report(zip_packet, node_id) do
Report.new(:complete, :command, node_id, command: Command.param!(zip_packet, :command))
end
defp do_send_command(command_runner, state) do
%State{transport: transport} = state
binary = CommandRunner.encode_command(command_runner)
Transport.send(transport, binary)
end
defp do_timeout_reply(waiter, grizzly_command) do
if grizzly_command.status == :queued do
{pid, _tag} = waiter
report =
Report.new(:complete, :timeout, grizzly_command.node_id,
command_ref: grizzly_command.ref,
queued: true
)
send(pid, {:grizzly, :report, report})
else
report =
Report.new(:complete, :timeout, grizzly_command.node_id, command_ref: grizzly_command.ref)
send(waiter, {:grizzly, :report, report})
end
end
end