Packages
grizzly
0.6.4
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/node.ex
defmodule Grizzly.Node do
@moduledoc """
Data structure representing a Z-Wave Node
"""
require Logger
alias Grizzly.{Conn, SeqNumber, Controller, CommandClass}
alias Grizzly.Conn.Config
alias Grizzly.Network.State, as: NetworkState
alias Grizzly.Node.Association
alias Grizzly.CommandClass.Association.Set, as: AssociationSet
alias Grizzly.CommandClass.ZipNd.InvNodeSolicitation
alias Grizzly.CommandClass.Version.CommandClassGet
@type node_id :: non_neg_integer()
@type association_opt :: {:network_state, NetworkState.state()} | {:extra_nodes, [node_id]}
@type security :: :none | :failed | :s0 | :s2_unauthenticated | :s2_authenticated
@type t :: %__MODULE__{
id: node_id(),
command_classes: [CommandClass.t()],
security: security(),
basic_cmd_class: atom() | nil,
generic_cmd_class: atom() | nil,
specific_cmd_class: atom() | nil,
ip_address: :inet.ip_address() | nil,
conn: Conn.t() | nil,
associations: [Association.t()],
listening?: boolean(),
security: security()
}
@enforce_keys [:id]
defstruct id: nil,
command_classes: [],
security: :none,
basic_cmd_class: nil,
generic_cmd_class: nil,
specific_cmd_class: nil,
ip_address: nil,
associations: [],
conn: nil,
listening?: false,
security: :none
@spec new(opts :: keyword) :: t
def new(opts \\ []) do
struct(__MODULE__, opts)
end
@doc """
Batch update a Node's fields with new info.
"""
@spec update(t, opts :: keyword() | map()) :: t
def update(zw_node, opts \\ []) do
struct(zw_node, opts)
end
@doc """
Adds the IP address to the node structure. If
there is an IP address already part of the
node this will override that field.
"""
@spec put_ip(t, :inet.ip_address()) :: t
def put_ip(zw_node, ip_address) do
%{zw_node | ip_address: ip_address}
end
@doc """
Get the Node's IP adress.
If the node does not have an IP address this function
will ask the Z-Wave network to provide the address.
Normally, it is good to called `put_ip/2` after this
function.
If the node has the IP address already, then this
function will return that address
"""
@spec get_ip(t() | node_id()) :: {:ok, :inet.ip_address()} | {:error, :nack_response}
def get_ip(%__MODULE__{ip_address: nil, id: node_id}) do
do_get_ip(node_id)
end
def get_ip(%__MODULE__{ip_address: ip_address}), do: {:ok, ip_address}
def get_ip(node_id) when is_integer(node_id) do
do_get_ip(node_id)
end
@doc """
Establish a connection with a Node
Pass in connection options to build a `Grizzly.Conn`, will use
`Grizzly.conn()` to build default values.
If the node does not have an IP address, this function will attempt to
retrieve that information from the Z-Wave network
"""
@spec connect(t, keyword) :: {:ok, t}
def connect(zw_node, opts \\ [])
def connect(%__MODULE__{ip_address: nil} = zw_node, opts) do
with {:ok, ip} <- get_ip(zw_node),
zw_node <- put_ip(zw_node, ip) do
connect(zw_node, opts)
end
end
def connect(zw_node, opts) do
config = make_config(zw_node, opts)
conn = Conn.open(config)
{:ok, %{initialize_command_versions(zw_node) | conn: conn}}
end
@spec disconnect(t) :: :ok
def disconnect(%__MODULE__{conn: conn}) do
Conn.close(conn)
end
@doc """
Make the Node's Conn.Config. This will use the `Grizzly.config()` function
but it will replace the ip_address field of that config with the Node's
IP address.
And optional keyword list of config overrides can be provided. See
`Grizzly.Conn.Config` module for the valid fields.
"""
@spec make_config(t, opts :: keyword) :: Config.t()
def make_config(%__MODULE__{ip_address: ip_address}, opts \\ []) do
struct(Grizzly.config(), [ip: ip_address] ++ opts)
end
@doc """
Checks to see if the node has a particular command class
"""
@spec has_command_class?(t, command_class :: atom) :: boolean
def has_command_class?(%__MODULE__{} = zw_node, command_class) do
command_class in command_class_names(zw_node)
end
@doc """
Whether a node is connected
"""
@spec connected?(t) :: boolean
def connected?(%__MODULE__{conn: conn, ip_address: ip_address}) do
conn != nil and ip_address != nil
end
@doc """
Get the list of names of command classes supported by the node.
"""
@spec command_class_names(t) :: [atom]
def command_class_names(%__MODULE__{command_classes: command_classes}) do
Enum.map(command_classes, fn %CommandClass{name: command_class_name} -> command_class_name end)
end
@doc """
Get the node's command class by name
"""
@spec command_class(t, atom) :: {:ok, CommandClass.t()} | {:error, :not_found}
def command_class(node, command_class_name) do
case Enum.find(node.command_classes, &(&1.name == command_class_name)) do
nil ->
{:error, :not_found}
command_class ->
{:ok, command_class}
end
end
@doc """
Update the command class versions of a node
"""
@spec update_command_class_versions(t) :: t()
def update_command_class_versions(%__MODULE__{command_classes: command_classes} = zw_node) do
command_classes =
Enum.map(command_classes, fn command_class ->
command_class_name = CommandClass.name(command_class)
case get_command_class_version(zw_node, command_class_name) do
{:ok, version} ->
CommandClass.set_version(command_class, version)
{:error, :timeout_get_command_class_version} ->
_ =
Logger.warn(
"Timeout getting the command class version of #{inspect(command_class_name)} for node #{
inspect(zw_node.id)
}"
)
command_class
{:error, :command_class_not_found} ->
_ =
Logger.warn(
"Unable to get command command class for #{inspect(command_class_name)} because the node (#{
inspect(zw_node.id)
}) does not support that command class"
)
command_class
{:error, :command_queued} ->
_ =
Logger.warn(
"Unable to get command command class for #{inspect(command_class_name)} because the node (#{
inspect(zw_node.id)
}) is asleep"
)
command_class
end
end)
%{zw_node | command_classes: command_classes}
end
@doc """
Get the version of a node's command class, if the node does not have a version for
this command class this function will try to get it from the Z-Wave network.
"""
@spec get_command_class_version(t(), CommandClass.name()) ::
{:ok, CommandClass.version()}
| {:error,
:command_class_not_found | :timeout_get_command_class_version | :command_queued}
def get_command_class_version(zw_node, command_class_name) do
with {:ok, cc} <- command_class(zw_node, command_class_name),
:no_version_number <- CommandClass.version(cc),
{:ok, %{version: version}} <- do_get_command_class_version(zw_node, command_class_name) do
{:ok, version}
else
{:ok, :queued, _reference} ->
{:error, :command_queued}
{:error, :not_found} ->
{:error, :command_class_not_found}
{:error, :timeout_get_command_class_version} = error ->
error
version when is_integer(version) ->
{:ok, version}
error ->
# This should nerve happen, and if it does something is
# really wrong in the system.
raise "Unmatch error when getting a command class version #{inspect(error)}"
end
end
@doc "Whether the versions of a node's command class are all known"
@spec command_class_versions_known?(t) :: boolean
def command_class_versions_known?(zw_node) do
not Enum.any?(
zw_node.command_classes,
&(&1.version == :no_version_number)
)
end
@doc """
Update a command class of a node.
"""
@spec update_command_class(t, CommandClass.t()) :: t
def update_command_class(
%__MODULE__{command_classes: command_classes} = zw_node,
command_class
) do
index = Enum.find_index(command_classes, &(&1.name == command_class.name))
updated_command_classes = List.replace_at(command_classes, index, command_class)
%__MODULE__{zw_node | command_classes: updated_command_classes}
end
@doc """
Put an Association into the association list of a node.
"""
@spec put_association(t, Association.t()) :: t
def put_association(%__MODULE__{associations: associations} = zw_node, association) do
%{zw_node | associations: associations ++ [association]}
end
@doc """
Gets the association list of a Node.t
"""
@spec get_association_list(t) :: [Association.t()]
def get_association_list(%__MODULE__{associations: associations}), do: associations
@doc """
Add a lifeline group association to this node.
By default the lifeline group contains the controller node (id `1`),
so the `extra_nodes` field is a list of node ids to also be added
to this node's lifeline group.
"""
@spec add_lifeline_group(t, [association_opt]) :: {:ok, t}
def add_lifeline_group(zw_node, association_opts \\ []) do
extra_nodes = Keyword.get(association_opts, :extra_nodes, [])
lifeline_association = Association.new(0x01, [0x01] ++ extra_nodes)
configure_association(zw_node, lifeline_association, association_opts)
end
@doc """
Given a Node and an Association configure the node to have association.
"""
@spec configure_association(t, Association.t(), [association_opt]) :: {:ok, t}
def configure_association(zw_node, association, association_opts \\ []) do
# TODO: clean up
_ = Logger.debug("CONFIGURING ASSOCIATION of node #{zw_node.id}")
# Don't associate devices that may be a sleep (else the command times out)
if has_command_class?(zw_node, :association) do
seq_number = SeqNumber.get_and_inc()
netstate = Keyword.get(association_opts, :network_state)
cmd_opts =
association
|> Association.to_keyword()
|> Keyword.put(:seq_number, seq_number)
|> Keyword.put(:exec_state, netstate)
case Grizzly.send_command(zw_node.conn, AssociationSet, cmd_opts) do
:ok ->
_ = Logger.debug("CONFIGURED association of node #{zw_node.id}")
{:ok, put_association(zw_node, association)}
{:error, reason} ->
_ =
Logger.warn(
"Unable to set association for node #{zw_node.id} because #{inspect(reason)}"
)
{:ok, zw_node}
end
else
{:ok, zw_node}
end
end
@doc """
Initialize a node's command classes with the default version
"""
@spec initialize_command_versions(t()) :: t()
def initialize_command_versions(%__MODULE__{command_classes: command_classes} = zw_node) do
versioned_command_classes = Enum.map(command_classes, &initialize_command_class_version(&1))
%__MODULE__{zw_node | command_classes: versioned_command_classes}
end
defp initialize_command_class_version(command_class_name) when is_atom(command_class_name) do
CommandClass.new(name: command_class_name)
end
defp initialize_command_class_version(%CommandClass{} = command_class) do
command_class
end
defp do_get_ip(node_id) do
seq_number = SeqNumber.get_and_inc()
command_result =
Grizzly.send_command(
Controller.conn(),
InvNodeSolicitation,
seq_number: seq_number,
node_id: node_id
)
case command_result do
{:ok, {:node_ip, _, ip_address}} ->
{:ok, ip_address}
{:error, _} = error ->
error
end
end
defp do_get_command_class_version(zw_node, command_class_name) do
task =
Task.async(fn ->
seq_number = SeqNumber.get_and_inc()
Grizzly.send_command(
zw_node,
CommandClassGet,
seq_number: seq_number,
command_class: command_class_name
)
end)
try do
# wait 5 secs
Task.await(task)
catch
:exit, error ->
# Task needs to be killed explicitly because we are trapping exits
_ = Task.shutdown(task, :brutal_kill)
_ =
Logger.error(
"[GRIZZLY] Exit trapped when getting version of command class #{command_class_name} of node #{
zw_node.id
}: #{inspect(error)}"
)
{:error, :timeout_get_command_class_version}
end
end
end