lib/grizzly/network.ex

defmodule Grizzly.Network do
  @moduledoc """
  Module for working with the Z-Wave network
  """

  alias Grizzly.{Associations, Connections, Report, SeqNumber, ZWave}
  alias Grizzly.ZWave.Command

  @typedoc """
  Options for when you want to reset the device

  - `:notify` - if the flag is set to true this will try to notify any node that
    is part of the lifeline association group (default `true`)
  """
  @type reset_opt() :: {:notify, boolean()}

  @type opt() :: {:node_id, ZWave.node_id()}

  @doc """
  Get a list of node ids from the network

  Just because a node id might be in the list does not mean the node is on the
  network. A device might have been reset or unpaired from the controller with
  out the controller knowing. However, in most use cases this shouldn't be an
  issue.

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec get_node_ids([opt()]) :: Grizzly.send_command_response()
  def get_node_ids(opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(node_id, :node_list_get, seq_number: seq_number)
  end

  @doc """
  Reset the Z-Wave controller

  This command takes a few seconds to run.

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec reset_controller([reset_opt() | opt()]) :: Grizzly.send_command_response()
  def reset_controller(opts \\ []) do
    # close all the connections before resetting the controller. It's okay
    # to blindly close all connections because when we send the command to
    # the controller Grizzly will automatically reconnect to the controller
    # at that point in time. We do this because the connections to the Z-Wave
    # devices are still reachable after being removed and we will still be
    # sending keep alive messages when we don't need to and will have
    # unnecessary connections hanging out just taking up resources.
    :ok = Connections.close_all()
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    case Grizzly.send_command(node_id, :default_set, [seq_number: seq_number], timeout: 10_000) do
      {:ok, %Report{type: :command, status: :complete}} = response ->
        maybe_notify_reset(response, opts)

      other ->
        other
    end
  end

  @doc """
  Delete a node from the network's provisioning list via the node's DSK

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec delete_node_provisioning(Grizzly.ZWave.DSK.t(), [opt()]) ::
          Grizzly.send_command_response()
  def delete_node_provisioning(dsk, opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(node_id, :node_provisioning_delete, seq_number: seq_number, dsk: dsk)
  end

  @doc """
  Get the nodes provisioning list information via the node's DSK

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec get_node_provisioning(Grizzly.ZWave.DSK.t(), [opt()]) ::
          Grizzly.send_command_response()
  def get_node_provisioning(dsk, opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(node_id, :node_provisioning_get, seq_number: seq_number, dsk: dsk)
  end

  @doc """
  A node to the network provisioning list

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec set_node_provisioning(
          Grizzly.ZWave.DSK.t(),
          [Grizzly.ZWave.SmartStart.MetaExtension.extension()],
          [opt()]
        ) :: Grizzly.send_command_response()
  def set_node_provisioning(dsk, meta_extensions, opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(
      node_id,
      :node_provisioning_set,
      seq_number: seq_number,
      dsk: dsk,
      meta_extensions: meta_extensions
    )
  end

  @doc """
  Add a long range device to the provisioning list
  """
  @spec add_long_range_device(Grizzly.ZWave.DSK.t(), [opt()]) :: Grizzly.send_command_response()
  def add_long_range_device(dsk, opts \\ []) do
    extensions = [
      bootstrapping_mode: :long_range,
      smart_start_inclusion_setting: :pending,
      advanced_joining: [:s2_unauthenticated, :s2_authenticated]
    ]

    set_node_provisioning(dsk, extensions, opts)
  end

  @doc """
  List all the nodes on the provisioning list

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec list_node_provisionings(integer(), [opt()]) :: Grizzly.send_command_response()
  def list_node_provisionings(remaining_counter, opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(
      node_id,
      :node_provisioning_list_iteration_get,
      seq_number: seq_number,
      remaining_counter: remaining_counter
    )
  end

  @doc """
  Remove a (presumably) failed node

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec remove_failed_node([opt()]) ::
          Grizzly.send_command_response()
  def remove_failed_node(opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(:gateway, :failed_node_remove, seq_number: seq_number, node_id: node_id)
  end

  @doc """
  Request a network update (network healing)

  Options

    * `:node_id` - If your controller is part of another controller's network
      you might want to issue network commands to that controller. By default
      this option will chose your controller.
  """
  @spec request_network_update([opt()]) ::
          Grizzly.send_command_response()
  def request_network_update(opts \\ []) do
    seq_number = SeqNumber.get_and_inc()
    node_id = node_id_from_opts(opts)

    Grizzly.send_command(node_id, :network_update_request, seq_number: seq_number)
  end

  defp node_id_from_opts(opts) do
    Keyword.get(opts, :node_id, :gateway)
  end

  defp maybe_notify_reset(response, opts) do
    {:ok, %Report{command: command}} = response

    case Command.param!(command, :status) do
      :done ->
        maybe_notify_reset(opts)
        response

      :busy ->
        response
    end
  end

  defp maybe_notify_reset(opts) do
    if Keyword.get(opts, :notify, true) do
      notify_reset()
    else
      :ok
    end
  end

  defp notify_reset() do
    # get the nodes in the lifeline group
    case Associations.get(1) do
      nil ->
        :ok

      association ->
        Enum.each(association.node_ids, fn node_id ->
          Grizzly.send_command(node_id, :device_reset_locally_notification)
        end)
    end
  end
end