lib/grizzly/zwave/commands/node_info_cached_report.ex

defmodule Grizzly.ZWave.Commands.NodeInfoCacheReport do
  @moduledoc """
  Report the cached node information

  This command is normally used to respond to the `NodeInfoCacheGet` command

  Params:

  - `:seq_number` - the sequence number of the network command, normally from
    from the `NodeInfoCacheGet` command (required)
  - `:status` - the status fo the node information (required)
  - `:age` - the age of the cache data. A number that is expressed `2 ^ n`
    minutes (required)
  - `:listening?` - if the node is listening node or sleeping node (required)
  - `:command_classes` - a list of lists of command classes tagged by security attributes (optional default empty
    list)
  - `:basic_device_class` - the basic device class (required)
  - `:generic_device_class` - the generic device class (required)
  - `:specific_device_class` - the specific device class (required)
  """
  @behaviour Grizzly.ZWave.Command

  import Bitwise

  alias Grizzly.ZWave.Command
  alias Grizzly.ZWave.CommandClasses.NetworkManagementProxy
  alias Grizzly.ZWave.{CommandClasses, DeviceClasses}

  @typedoc """
  The status of the refresh of the node information cache

  Status:
    - `:ok` - the requested node id could found and up-to-date information is
      returned
    - `:not_responding` - the requested node id could be found but fresh
      information could not be retrieved
    - `:unknown` - the node id is unknown
  """
  @type status :: :ok | :not_responding | :unknown

  @type tagged_command_classes ::
          {:non_secure_supported, [CommandClasses.command_class()]}
          | {:non_secure_controlled, [CommandClasses.command_class()]}
          | {:secure_supported, [CommandClasses.command_class()]}
          | {:secure_controlled, [CommandClasses.command_class()]}
  @type param ::
          {:seq_number, Grizzly.seq_number()}
          | {:status, status()}
          | {:age, 1..14}
          | {:listening?, boolean()}
          | {:command_classes, [tagged_command_classes]}
          | {:basic_device_class, DeviceClasses.basic_device_class()}
          | {:generic_device_class, DeviceClasses.generic_device_class()}
          | {:specific_device_class, DeviceClasses.specific_device_class()}

  @impl true
  @spec new([param]) :: {:ok, Grizzly.ZWave.Command.t()}
  def new(params) do
    # TODO validate params
    command = %Command{
      name: :node_info_cache_report,
      command_byte: 0x04,
      command_class: NetworkManagementProxy,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl true
  def encode_params(command) do
    seq_number = Command.param!(command, :seq_number)
    status_byte = encode_status(Command.param!(command, :status))
    age = Command.param!(command, :age)
    listening_byte = encode_listening?(Command.param!(command, :listening?))
    command_classes = Command.param!(command, :command_classes)

    basic_device_class_byte =
      DeviceClasses.basic_device_class_to_byte(Command.param!(command, :basic_device_class))

    generic_device_class = Command.param!(command, :generic_device_class)

    specific_device_class_byte =
      DeviceClasses.specific_device_class_to_byte(
        generic_device_class,
        Command.param!(command, :specific_device_class)
      )

    optional_functionality_byte = encode_optional_functionality_byte(command_classes)

    # the `0x00` byte is a reserved byte for Z-Wave and must be set to 0x00
    <<seq_number, status_byte ||| age, listening_byte, optional_functionality_byte, 0x00,
      basic_device_class_byte, DeviceClasses.generic_device_class_to_byte(generic_device_class),
      specific_device_class_byte>> <> CommandClasses.command_class_list_to_binary(command_classes)
  end

  @impl true
  def decode_params(
        <<seq_number, status::size(4), age::size(4), list?::size(1), _::size(7), _, _keys,
          basic_device_class_byte, generic_device_class_byte, specific_device_class_byte,
          command_classes::binary>>
      ) do
    {:ok, basic_device_class} =
      DeviceClasses.basic_device_class_from_byte(basic_device_class_byte)

    {:ok, generic_device_class} =
      DeviceClasses.generic_device_class_from_byte(generic_device_class_byte)

    {:ok, specific_device_class} =
      DeviceClasses.specific_device_class_from_byte(
        generic_device_class,
        specific_device_class_byte
      )

    {:ok,
     [
       seq_number: seq_number,
       basic_device_class: basic_device_class,
       generic_device_class: generic_device_class,
       specific_device_class: specific_device_class,
       listening?: bit_to_bool(list?),
       command_classes: CommandClasses.command_class_list_from_binary(command_classes),
       status: decode_status(status),
       age: age
     ]}
  end

  def encode_status(_), do: 0
  def encode_command_classes(_), do: 0

  def encode_listening?(true), do: 0x80
  def encode_listening?(false), do: 0x00

  def bit_to_bool(bit), do: bit == 1

  def encode_optional_functionality_byte([]), do: 0x00
  def encode_optional_functionality_byte(_), do: 0x80

  def decode_command_classes(""), do: []
  def decode_command_classes(_), do: []

  def decode_status(0x00), do: :ok
  def decode_status(0x01), do: :not_responding
  def decode_status(0x02), do: :unknown
end