defmodule Grizzly.Inclusions do
@moduledoc """
Module for adding and removing Z-Wave nodes
In Z-Wave the term "inclusions" means two things:
1. Adding a new Z-Wave device to the Z-Wave Network
2. Removing a Z-Wave device to the Z-Wave Network
In practice though it is more common to speak about adding a Z-Wave node in
the context of "including" and removing an Z-Wave mode in the context of
"excluding." This module provides functionality for working will all contexts
of inclusion, both adding and removing.
## Adding a Z-Wave Node (including)
When adding a device that does not required any security authentication is
as simple as calling `Grizzly.Inclusions.add_node/0`.
```elixir
iex> Grizzly.Inclusions.add_node()
:ok
```
After starting the inclusion on the controller, which the above function
does, you can then put your device into inclusion as well. From here the new
device and your controller will communicate and if all goes well you should
receive a message in the form of
`{:grizzly, :inclusion, NodeAddStatus}` where the the `NodeAddStatus` is a
Z-Wave command the contains information about the inclusion status (status,
node id, supported command classes, security levels, etc.). See
`Grizzly.ZWave.Commands.NodeAddStatus` for more information about the values
in that command. For example:
```elixir
defmodule MyInclusionServer do
use GenServer
require Logger
alias Grizzly.Inclusions
alias Grizzly.ZWave.Command
def start_link(_) do
GenServer.start_link(__MODULE__, nil)
end
def add_node(pid) do
GenServer.call(pid, :add_node)
end
def init(_) do
{:ok, nil}
end
def handle_call(:add_node, _from, state) do
:ok = Inclusions.add_node()
{:reply, :ok, state}
end
def handle_info({:grizzly, :inclusion, report}, state) do
case Command.param!(report.command, :status) do
:done ->
node_id = Command.param!(report.command, :node_id)
Logger.info("Node added with id: " <> node_id)
:failed ->
Logger.warning("Adding node failed :(")
:security_failed ->
node_id = Command.param!(report.command, :node_id)
Logger.warning("Node added with id: " <> node_id <> "but the security failed")
end
{:noreply, state}
end
end
```
### Stop Adding a Node
If you need you need to stop trying to add a node to the Z-Wave network you
can use the `Grizzly.Inclusions.remove_node/0` function.
This should stop the controller from trying to add a node and return it to
a normal functions state.
### Security
There are five security levels in Z-Wave: unsecured, S0, S2 unauthenticated,
S2 authenticated, and S2 access control. The first 2 requires nothing
special from the calling process to able to use, as the controller and the
including node will figure out which security scheme to use.
#### S2
The process of adding an S2 device is a little more involved. The process is
the same up until right after you put the including node into the inclusion
mode. At that point including will request security keys, which really means
it tells you which S2 security scheme it supports. You then use the
`Grizzly.Inclusions.grant_keys/1` function to pass a list of allowed security
schemes.
After that the node will response with a `NodeAddDSKReport` where it reports
the DSK and something called the `:dsk_input_length`. If the input length is
`0`, that means it is trying to do S2 unauthenticated inclusion. You can
just call `Grizzly.Inclusions.set_input_dsk/0` function and the rest of the
inclusion process should continue until complete.
If the `:dsk_input_length` has number, normally will be `2` that means the
including device is requesting a 5 digit digit pin that is normally found on
a label somewhere on the physical device it.
From here you can call `Grizzly.Inclusions.set_input_dsk/1` with the 5 digit
integer as the argument. The inclusion process should continue until complete.
## Removing a Z-Wave Node (excluding)
To remove a Z-Wave node from the network the
`Grizzly.Inclusions.remove_node/0` will start an inclusion process for removing
a Z-Wave node. After calling this function you can place your device into the
inclusion (normally the same way you included the device is the way the device
is excluded) mode. At the end of the exclusion the `NodeRemoveStatus` command
is received and can be inspected for success of failure.
### Removed Node ID 0?
Any Z-Wave controller can excluded a device from another controller. In
practice this means your Z-Wave controller can make a device "forget" the
controller it is currently attached to. Most the time Z-Wave products will
have you excluded your device and then included just to make sure the
including node isn't connected to another Z-Wave controller.
When this happens you will a successful `NodeRemoveStatusReport` but the node
id will be `0`. This is consider successful and most the time intend.
## Stopping Remove Node Process
To stop the removal inclusion process on your controller you can call the
`Grizzly.Inclusions.remove_node_stop/0` function.
## Inclusion Handler
To tie into the inclusion process we default to sending messages to the
calling process. However, there is a better way to tie into this system.
When starting any inclusion process you can pass the `:handler` option
which can be either another pid or a module that implements the
`Grizzly.InclusionHandler` behaviour, or a tuple with the module and callback arguments.
A basic implementation might look like:
```elixir
defmodule MyApp.InclusionHandler do
@behaviour Grizzly.InclusionHandler
require Logger
def handle_report(report, opts) do
Logger.info("Got command: " <> report.command.name <> " with callback arguments " <> inspect opts)
:ok
end
end
```
This is recommended for applications using Grizzly over a `GenServer` that
wraps `Grizzly.Inclusions`.
"""
alias Grizzly.Inclusions.StatusServer
alias Grizzly.InclusionServer
alias Grizzly.ZWave.{DSK, Security}
@typedoc """
Status of the inclusion server
"""
@type status() ::
:idle
| :node_adding
| :node_add_stopping
| :node_removing
| :node_remove_stopping
| :waiting_dsk
| :waiting_s2_keys
| :s2_keys_granted
| :dsk_input_set
| :learn_mode
| :learn_mode_stopping
@typedoc """
Options for inclusion
"""
@type opt() ::
{:controller_id, Grizzly.node_id()}
| {:timeout, non_neg_integer()}
| {:handler, pid() | module() | {module, keyword()}}
| {atom(), term()}
@doc """
Start the process to add a Z-Wave node to the network
"""
@spec add_node([opt()]) :: :ok | status()
def add_node(opts \\ []) do
InclusionServer.add_node(opts)
end
@doc """
Start the process to remove a Z-Wave node from the network
"""
@spec remove_node([opt()]) :: :ok | status()
def remove_node(opts \\ []) do
InclusionServer.remove_node(opts)
end
@doc """
Get the current status of the inclusion process
"""
@spec status() :: status()
def status() do
StatusServer.get()
end
@doc """
Tell the inclusion process which keys to use during the inclusion process
During S2 inclusion the node being included with send a `DSKAddKeysReport`
to request which keys it can use to included securely. This function is
useful for passing back to the node which keys it is allowed to use and
depending on that answer the including node might request more information.
"""
@spec grant_keys([Security.key()]) :: :ok | status()
def grant_keys(s2_keys) do
InclusionServer.grant_keys(s2_keys)
end
@doc """
Tell the inclusion process what the input DSK is
If the `NodeAddDSKReport`'s `:input_dsk_length` is `0` you can just call this
function without any arguments:
```elixir
Grizzly.Inclusions.set_input_dsk()
```
If you are doing `:s2_authenticated` or `:s2_access_control` the
`NodeAddDSKReport` will probably ask for input DSK length of `2`. This means
it is expecting a 2 byte (16 bit) number, which is normally a 5 digit pin
located somewhere on the node that is being added. After locating the pin and
you can pass it as an argument like so:
```elixir
{:ok, dsk} = Grizzly.ZWave.DSK.parse("12345")
Grizzly.Inclusions.set_input_dsk(dsk)
```
"""
@spec set_input_dsk(DSK.t()) :: :ok | status()
def set_input_dsk(input_dsk \\ DSK.new(<<>>)) do
InclusionServer.set_input_dsk(input_dsk)
end
@doc """
Stop an add node inclusion process
"""
@spec add_node_stop() :: :ok | status()
def add_node_stop() do
InclusionServer.add_node_stop()
end
@doc """
Stop a remove node inclusion process
"""
@spec remove_node_stop() :: :ok | status()
def remove_node_stop() do
InclusionServer.remove_node_stop()
end
@doc """
Start learn mode on the controller
"""
@spec learn_mode([opt()]) :: :ok | status()
def learn_mode(opts \\ []) do
InclusionServer.learn_mode(opts)
end
@doc """
Stop learn mode on the controller
"""
@spec learn_mode_stop() :: :ok | status()
def learn_mode_stop() do
InclusionServer.learn_mode_stop()
end
@doc """
Stop the inclusion runner
"""
@deprecated "Use either remove_node_stop/0, add_node_stop/0, or learn_mode_stop/0"
@spec stop() :: :ok
def stop() do
:ok
end
@doc """
Check to see if there is an inclusion process running
"""
@spec inclusion_running?() :: boolean()
def inclusion_running?() do
StatusServer.get() != :idle
end
end