Packages
grizzly
8.4.0
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/commands/command.ex
defmodule Grizzly.Commands.Command do
@moduledoc false
require Logger
# Data structure for working with Z-Wave commands as they relate to the
# Grizzly runtime
alias Grizzly.CommandHandlers.SupervisionReport
alias Grizzly.Commands.Table
alias Grizzly.{Report, SeqNumber, ZWave}
alias Grizzly.ZWave.Command, as: ZWaveCommand
alias Grizzly.ZWave.Commands.{SupervisionGet, ZIPPacket}
@type status :: :inflight | :queued | :complete
@type t :: %__MODULE__{
owner: pid(),
retries: non_neg_integer(),
source: ZWaveCommand.t(),
handler_state: any(),
handler: module(),
seq_number: Grizzly.seq_number(),
timeout_ref: reference() | nil,
ref: reference(),
status: status(),
with_transmission_stats: boolean(),
transmission_stats: keyword(),
node_id: ZWave.node_id(),
supervision?: boolean(),
more_info?: boolean(),
session_id: non_neg_integer() | nil,
acknowledged: boolean()
}
@type opt ::
{:timeout_ref, reference()}
| {:reference, reference()}
| {:retries, non_neg_integer()}
| {:transmission_stats, boolean()}
defstruct owner: nil,
retries: 0,
source: nil,
handler_state: nil,
handler: nil,
seq_number: nil,
timeout_ref: nil,
ref: nil,
status: :inflight,
with_transmission_stats: false,
transmission_stats: [],
node_id: nil,
supervision?: false,
more_info?: false,
session_id: nil,
acknowledged: false
@spec from_zwave_command(ZWaveCommand.t(), ZWave.node_id(), pid(), [opt()]) :: t()
def from_zwave_command(zwave_command, node_id, owner, opts \\ []) do
retries = Keyword.get(opts, :retries, 0)
command_ref = Keyword.get(opts, :reference, make_ref())
timeout_ref = Keyword.get(opts, :timeout_ref)
with_transmission_stats = Keyword.get(opts, :transmission_stats, false)
more_info = Keyword.get(opts, :more_info, false)
{zwave_command, handler, handler_init_args, supervision?, session_id} =
if use_supervision?(zwave_command, opts) do
zwave_command = add_supervision_encapsulation(zwave_command, node_id, opts)
session_id = ZWaveCommand.param!(zwave_command, :session_id)
handler_init_args = [
session_id: session_id,
node_id: node_id,
command_ref: command_ref,
waiter: Keyword.get(opts, :waiter),
status_updates?: Keyword.get(opts, :status_updates?, false)
]
{zwave_command, SupervisionReport, handler_init_args, true, session_id}
else
_ = maybe_warn_supervision(zwave_command, opts[:supervision?])
{handler, handler_init_args} = get_handler_spec(zwave_command, opts)
{zwave_command, handler, handler_init_args, false, nil}
end
{:ok, handler_state} = handler.init(handler_init_args)
%__MODULE__{
handler: handler,
handler_state: handler_state,
source: zwave_command,
owner: owner,
seq_number: get_seq_number(zwave_command),
timeout_ref: timeout_ref,
retries: retries,
ref: command_ref,
with_transmission_stats: with_transmission_stats,
node_id: node_id,
supervision?: supervision?,
session_id: session_id,
more_info?: more_info
}
end
@spec to_binary(t()) :: binary()
def to_binary(command) do
zwave_command = command.source
case zwave_command.name do
:keep_alive ->
<<zwave_command.command_class.byte(), zwave_command.command_byte>> <>
ZWaveCommand.encode_params(zwave_command)
_other ->
opts = make_zip_packet_command_opts(command)
{:ok, zip_packet_command} =
ZIPPacket.with_zwave_command(zwave_command, command.seq_number, opts)
ZWaveCommand.to_binary(zip_packet_command)
end
end
@spec handle_zip_command(t(), ZWaveCommand.t()) ::
{Report.t(), t()}
| {:retry, t()}
| {:continue, t()}
def handle_zip_command(command, zip_command) do
case ZWaveCommand.param!(zip_command, :flag) do
:ack_response ->
handle_ack_response(command, zip_command)
:nack_waiting ->
handle_nack_waiting(command, zip_command)
flag when flag in [:nack_response, :nack_queue_full] ->
handle_final_nack(command, zip_command)
flag when flag in [nil, :ack_request] ->
do_handle_zip_command(command, zip_command)
end
end
defp handle_ack_response(command, zip_packet) do
seq_number = ZWaveCommand.param!(zip_packet, :seq_number)
if command.seq_number == seq_number do
do_handle_ack_response(%__MODULE__{command | acknowledged: true}, zip_packet)
else
{:continue, command}
end
end
defp do_handle_ack_response(command, zip_packet) do
transmission_stats = make_network_stats(command, zip_packet)
case command.handler.handle_ack(command.handler_state) do
{:continue, new_handler_state} ->
{:continue,
%__MODULE__{
command
| handler_state: new_handler_state,
transmission_stats: transmission_stats
}}
{:complete, response} ->
build_complete_reply(
%__MODULE__{command | transmission_stats: transmission_stats},
response
)
end
end
# Handles both nack response and nack queue full
defp handle_final_nack(command, zip_packet) do
flag = ZWaveCommand.param!(zip_packet, :flag)
seq_number = ZWaveCommand.param!(zip_packet, :seq_number)
cond do
command.seq_number != seq_number ->
{:continue, command}
# Never retry on a nack_queue_full
flag == :nack_queue_full ->
make_queue_full_response(command)
command.retries > 0 ->
{:retry, %__MODULE__{command | retries: command.retries - 1}}
true ->
make_nack_response(command)
end
end
defp handle_nack_waiting(command, zip_packet) do
seq_number = ZWaveCommand.param!(zip_packet, :seq_number)
if command.seq_number == seq_number do
# SDS13784 Network specification states that a default of 90 seconds
# should be used if no expected delay is provided.
make_queued_response(command, zip_packet)
else
{:continue, command}
end
end
defp do_handle_zip_command(command, zip_packet_command) do
zwave_command = ZWaveCommand.param!(zip_packet_command, :command)
case command.handler.handle_command(zwave_command, command.handler_state) do
{:continue, new_handler_state} ->
{:continue, %__MODULE__{command | handler_state: new_handler_state}}
{:complete, response} ->
build_complete_reply(command, response)
end
end
defp get_handler_spec(zwave_command, opts) do
case Keyword.get(opts, :handler) do
nil ->
Table.handler(zwave_command.name)
handler ->
Table.format_handler_spec(handler)
end
end
defp maybe_warn_supervision(command, true) do
Logger.warning(
"[Grizzly] Supervision was requested for command #{command.name} but is not supported"
)
end
defp maybe_warn_supervision(_, _), do: :ok
defp use_supervision?(zwave_command, opts) do
opts[:supervision?] == true && Table.supports_supervision?(zwave_command.name)
end
defp add_supervision_encapsulation(zwave_command, node_id, opts) do
if Keyword.get(opts, :supervision?) do
encapsulated_command = ZWaveCommand.to_binary(zwave_command)
{:ok, command} =
SupervisionGet.new(
status_updates: :one_now_more_later,
session_id: Grizzly.SessionId.get_and_inc(node_id),
encapsulated_command: encapsulated_command
)
command
else
zwave_command
end
end
defp get_seq_number(zwave_command) do
case ZWaveCommand.param(zwave_command, :seq_number) do
nil ->
SeqNumber.get_and_inc()
seq_number ->
seq_number
end
end
defp make_queued_response(command, zip_packet) do
case ZIPPacket.extension(zip_packet, :expected_delay, 90) do
delay when delay > 1 ->
make_queued_or_queued_ping_response(command, delay)
_other ->
{:continue, command}
end
end
defp make_queued_or_queued_ping_response(command, delay) do
case command.status do
:inflight ->
queued_delay_report =
Report.new(:inflight, :queued_delay, command.node_id,
command_ref: command.ref,
queued_delay: delay,
queued: true
)
{queued_delay_report, %__MODULE__{command | status: :queued}}
:queued ->
{Report.new(:inflight, :queued_ping, command.node_id,
command_ref: command.ref,
queued_delay: delay,
queued: true
), command}
end
end
defp make_nack_response(command) do
{Report.new(:complete, :nack_response, command.node_id,
command_ref: command.ref,
queued: command.status == :queued,
transmission_stats: command.transmission_stats
), %__MODULE__{command | status: :complete}}
end
defp make_queue_full_response(command) do
{Report.new(:complete, :queue_full, command.node_id,
command_ref: command.ref,
queued: command.status == :queued,
transmission_stats: command.transmission_stats
), %__MODULE__{command | status: :complete}}
end
defp build_complete_reply(command, response) do
case command.status do
:inflight ->
{build_report(command, response), %__MODULE__{command | status: :complete}}
:queued ->
case response do
:ok ->
{Report.new(:complete, :ack_response, command.node_id,
command_ref: command.ref,
acknowledged: true,
queued: true,
transmission_stats: command.transmission_stats
), %__MODULE__{command | status: :complete}}
%ZWaveCommand{} ->
{Report.new(:complete, :command, command.node_id,
command_ref: command.ref,
acknowledged: command.acknowledged,
command: response,
queued: true,
transmission_stats: command.transmission_stats
), %__MODULE__{command | status: :complete}}
end
end
end
defp make_zip_packet_command_opts(grizzly_command) do
Keyword.new()
|> maybe_add_installation_and_maintenance_get(grizzly_command)
|> maybe_add_more_info_flag(grizzly_command)
|> add_seq_number(grizzly_command)
end
defp maybe_add_installation_and_maintenance_get(opts, grizzly_command) do
if grizzly_command.with_transmission_stats do
Keyword.put(opts, :header_extensions, [:installation_and_maintenance_get])
else
opts
end
end
defp maybe_add_more_info_flag(opts, grizzly_command) do
if grizzly_command.more_info? do
Keyword.put(opts, :more_info, true)
else
opts
end
end
defp add_seq_number(opts, grizzly_command) do
Keyword.put(opts, :seq_number, grizzly_command.seq_number)
end
defp make_network_stats(command, zip_packet) do
Keyword.new()
|> maybe_return_network_stats(command, zip_packet)
end
defp maybe_return_network_stats(meta, command, zip_packet) do
if command.with_transmission_stats do
stats = get_stats_from_zip_packet(zip_packet)
Enum.reduce(stats, meta, fn
{:last_working_route, routes, speed}, m ->
Keyword.put(m, :last_working_route, routes) |> Keyword.put(:transmission_speed, speed)
# Note: `zipgateway` >= v7.15 does not support the dynamic power level stats
# and all will be marked as `:not_available`.
# These stats include:
# * `:outgoing_rssi_hops`
# * `:local_noise_floor`
# * `:remote_noise_floor`
# * `:local_node_tx_power`
# * `:remote_node_tx_power`
#
# So we just filter these fields out here not to confuse users as to
# which stats to use.
{local_power_field, _value, _power_field, _remote_value}, meta
when local_power_field in [:local_node_tx_power, :local_noise_floor] ->
meta
{:outgoing_rssi_hops, _hops}, meta ->
meta
{key, value}, m ->
Keyword.put(m, key, value)
end)
|> calculate_rssi_values()
else
meta
end
end
defp calculate_rssi_values(stats) do
min_rssi =
Keyword.get(stats, :rssi_hops, [])
|> Enum.filter(&is_number/1)
|> Enum.min(fn -> :not_available end)
Keyword.put(stats, :rssi_dbm, min_rssi)
|> Keyword.put(:rssi_4bars, calculate_bars(min_rssi))
end
# These values were determined based on -78dBm being the lowest RSSI considered
# to be 4 bars, and -97 being the lowest value for 1 bar. The thresholds for
# 2 and 3 bars were calculated using a logarithmic regression model represented
# in the following R script:
#
# rssi <- c(-78, -97)
# bars <- c(4, 1)
# model <- lm(bars ~ log(abs(rssi), 2))
# coeffs <- coef(model)
# a <- coeffs[1]
# b <- coeffs[2]
# bars_to_rssi <- function(bars) round((2 ^ ((bars - a) / b)) * -1)
# cat("1 bar >=", bars_to_rssi(1), "dBm\n")
# cat("2 bars >=", bars_to_rssi(2), "dBm\n")
# cat("3 bars >=", bars_to_rssi(3), "dBm\n")
# cat("4 bars >=", bars_to_rssi(4), "dBm\n")
defp calculate_bars(:not_available), do: :not_available
defp calculate_bars(rssi) when rssi >= -78, do: 4
defp calculate_bars(rssi) when rssi >= -84, do: 3
defp calculate_bars(rssi) when rssi >= -90, do: 2
defp calculate_bars(rssi) when rssi >= -97, do: 1
defp calculate_bars(_rssi), do: 0
defp get_stats_from_zip_packet(zip_packet) do
case ZIPPacket.extension(zip_packet, :installation_and_maintenance_report) do
nil ->
[]
report ->
report
end
end
defp build_report(command, :ok) do
%Report{
status: :complete,
command_ref: command.ref,
transmission_stats: command.transmission_stats,
type: :ack_response,
node_id: command.node_id
}
end
defp build_report(command, response) do
%Report{
status: :complete,
command: response,
transmission_stats: command.transmission_stats,
type: :command,
command_ref: command.ref,
node_id: command.node_id
}
end
end