lib/grizzly/zwave/commands/configuration_properties_report.ex

defmodule Grizzly.ZWave.Commands.ConfigurationPropertiesReport do
  @moduledoc """
  This command is used to advertise the properties of a configuration parameter.

  Params:

    * `:param_number` - This field is used to specify which configuration parameter (required)

    * `read_only` - This field is used to indicate if the parameter is read-only. (optional - v4+)

    * `:altering_capabilities`: - This field is used to indicate if the advertised parameter triggers a change in the node’s capabilities. (optional - v4+)

    * `:format` - This field is used to advertise the format of the parameter, one of :signed_integer, :unsigned_integer, :enumerated, :bit_field (required)

    * `:size` - This field is used to advertise the size of the actual parameter, one of 0, 1, 2, 4
                The advertised size MUST also apply to the fields “Min Value”, “Max Value”, “Default Value” carried in
                this command. (required)

    * `:min_value` - This field advertises the minimum value that the actual parameter can assume.
                     If the parameter is “Bit field”, this field MUST be set to 0. (required if size > 0 else omitted)

    * `:max_value` - This field advertises the maximum value that the actual parameter can assume.
                     If the parameter is “Bit field”, each individual supported bit MUST be set to ‘1’, while each un-
                     supported bit of MUST be set to ‘0’. (required if size > 0 else omitted)

    * `:default_value` - This field MUST advertise the default value of the actual parameter (required if size > 0 else omitted)

    * `:next_param_number` - This field advertises the next available (possibly non-sequential) configuration parameter, else 0 (required)

    * `:advanced` - This field is used to indicate if the advertised parameter is to be presented in the “Advanced”
                    parameter section in the controller GUI. (optional - v4+)

    * `:no_bulk_support` - This field is used to advertise if the sending node supports Bulk Commands. (optional - v4+)

  """

  @behaviour Grizzly.ZWave.Command

  alias Grizzly.ZWave.{Command, DecodeError}
  alias Grizzly.ZWave.CommandClasses.Configuration

  @type param ::
          {:param_number, non_neg_integer()}
          | {:read_only, boolean | nil}
          | {:altering_capabilities, boolean | nil}
          | {:format, Configuration.format()}
          | {:size, 0 | 1 | 2 | 4}
          | {:min_value, integer | nil}
          | {:max_value, integer | nil}
          | {:default_value, integer | nil}
          | {:next_param_number, non_neg_integer()}
          | {:advanced, boolean | nil}
          | {:no_bulk_support, boolean | nil}

  @impl true
  @spec new([param()]) :: {:ok, Command.t()}
  def new(params) do
    command = %Command{
      name: :configuration_properties_report,
      command_byte: 0x0F,
      command_class: Configuration,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl true
  @spec encode_params(Command.t()) :: binary()
  def encode_params(command) do
    param_number = Command.param!(command, :param_number)
    next_param_number = Command.param!(command, :next_param_number)
    format = Command.param!(command, :format)
    format_byte = Configuration.format_to_byte(format)
    size = Command.param!(command, :size) |> Configuration.validate_size()

    read_only_bit = Command.param(command, :read_only, false) |> Configuration.boolean_to_bit()

    altering_capabilities_bit =
      Command.param(command, :altering_capabilities, false) |> Configuration.boolean_to_bit()

    <<param_number::size(16), altering_capabilities_bit::size(1), read_only_bit::size(1),
      format_byte::size(3),
      size::size(3)>> <>
      maybe_value_specs(command, format, size) <>
      <<next_param_number::size(16)>> <> maybe_v4_end_byte(command)
  end

  @impl true
  @spec decode_params(binary()) :: {:ok, [param()]} | {:error, DecodeError.t()}
  def decode_params(
        <<param_number::size(16), altering_capabilities_bit::size(1), read_only_bit::size(1),
          format_byte::size(3), 0x00::size(3), next_param_number::size(16), maybe_more::binary>>
      ) do
    with {:ok, format} <- Configuration.format_from_byte(format_byte) do
      case maybe_more do
        # < v4
        <<>> ->
          {:ok,
           [
             param_number: param_number,
             format: format,
             size: 0,
             next_param_number: next_param_number
           ]}

        <<_reserved::size(6), no_bulk_support_bit::size(1), advanced_bit::size(1)>> ->
          {:ok,
           [
             param_number: param_number,
             read_only: Configuration.boolean_from_bit(read_only_bit),
             advanced: Configuration.boolean_from_bit(advanced_bit),
             no_bulk_support: Configuration.boolean_from_bit(no_bulk_support_bit),
             altering_capabilities: Configuration.boolean_from_bit(altering_capabilities_bit),
             format: format,
             size: 0,
             next_param_number: next_param_number
           ]}
      end
    else
      {:error, %DecodeError{} = decode_error} ->
        {:error, %DecodeError{decode_error | command: :configuration_properties_report}}
    end
  end

  def decode_params(
        <<param_number::size(16), altering_capabilities_bit::size(1), read_only_bit::size(1),
          format_byte::size(3), size::size(3), min_value_bin::binary-size(size),
          max_value_bin::binary-size(size), default_value_bin::binary-size(size),
          next_param_number::size(16), maybe_more::binary>>
      ) do
    with {:ok, format} <- Configuration.format_from_byte(format_byte) do
      value_specs = [
        min_value: value_spec(size, format, min_value_bin),
        max_value: value_spec(size, format, max_value_bin),
        default_value: value_spec(size, format, default_value_bin)
      ]

      case maybe_more do
        # < v4
        <<>> ->
          {:ok,
           [
             param_number: param_number,
             format: format,
             size: size,
             next_param_number: next_param_number
           ]
           |> Keyword.merge(value_specs)}

        <<_reserved::size(6), no_bulk_support_bit::size(1), advanced_bit::size(1)>> ->
          {:ok,
           [
             param_number: param_number,
             read_only: Configuration.boolean_from_bit(read_only_bit),
             advanced: Configuration.boolean_from_bit(advanced_bit),
             no_bulk_support: Configuration.boolean_from_bit(no_bulk_support_bit),
             altering_capabilities: Configuration.boolean_from_bit(altering_capabilities_bit),
             format: format,
             size: size,
             next_param_number: next_param_number
           ]
           |> Keyword.merge(value_specs)}
      end
    else
      {:error, %DecodeError{} = decode_error} ->
        {:error, %DecodeError{decode_error | command: :configuration_properties_report}}
    end
  end

  defp maybe_value_specs(_command, _format, 0), do: <<>>

  defp maybe_value_specs(command, format, size) do
    min_value = Command.param!(command, :min_value)
    max_value = Command.param!(command, :max_value)
    default_value = Command.param!(command, :default_value)

    case format do
      :signed_integer ->
        <<min_value::integer-signed-unit(8)-size(size),
          max_value::integer-signed-unit(8)-size(size),
          default_value::integer-signed-unit(8)-size(size)>>

      _other ->
        <<min_value::integer-unsigned-unit(8)-size(size),
          max_value::integer-unsigned-unit(8)-size(size),
          default_value::integer-unsigned-unit(8)-size(size)>>
    end
  end

  defp maybe_v4_end_byte(command) do
    v4? =
      Command.param(command, :read_only) != nil or
        Command.param(command, :altering_capabilities) != nil or
        Command.param(command, :advanced) != nil or
        Command.param(command, :no_bulk_support) != nil

    if v4? do
      no_bulk_support_bit =
        Command.param!(command, :no_bulk_support) |> Configuration.boolean_to_bit()

      advanced_bit = Command.param!(command, :advanced) |> Configuration.boolean_to_bit()
      <<0x00::size(6), no_bulk_support_bit::size(1), advanced_bit::size(1)>>
    else
      <<>>
    end
  end

  defp value_spec(size, format, value_bin) do
    case format do
      :signed_integer ->
        <<value::integer-signed-unit(8)-size(size)>> = value_bin
        value

      _other ->
        <<value::integer-unsigned-unit(8)-size(size)>> = value_bin
        value
    end
  end
end