lib/grizzly/zwave/commands/window_covering_supported_report.ex

defmodule Grizzly.ZWave.Commands.WindowCoveringSupportedReport do
  @moduledoc """
  This command is used to advertise the supported properties of a windows covering device.

  Params:

    * `:parameter_names` - names of parameters supported by the device

  """

  @behaviour Grizzly.ZWave.Command

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

  @type param :: {:parameter_names, [WindowCovering.parameter_name()]}

  @impl Grizzly.ZWave.Command
  @spec new([param()]) :: {:ok, Command.t()}
  def new(params) do
    command = %Command{
      name: :window_covering_supported_report,
      command_byte: 0x02,
      command_class: WindowCovering,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl Grizzly.ZWave.Command
  @spec encode_params(Command.t()) :: binary()
  def encode_params(command) do
    parameter_names = Command.param!(command, :parameter_names)

    parameter_ids =
      for parameter_name <- parameter_names,
          do: WindowCovering.encode_parameter_name(parameter_name)

    parameter_masks_binary = parameter_ids_to_bitmasks(parameter_ids)
    <<0x00::size(4), byte_size(parameter_masks_binary)::size(4)>> <> parameter_masks_binary
  end

  @impl Grizzly.ZWave.Command
  @spec decode_params(binary()) :: {:ok, [param()]} | {:error, DecodeError.t()}
  def decode_params(
        <<_reserved::size(4), number_of_parameter_masks::size(4), parameter_masks::binary>>
      ) do
    case bitmasks_to_parameter_names(parameter_masks, number_of_parameter_masks) do
      {:ok, parameter_names} ->
        {:ok, [parameter_names: parameter_names]}

      {:error, :invalid_masks} ->
        {:error,
         %DecodeError{
           value: parameter_masks,
           param: :parameter_ids,
           command: :window_covering_supported_report
         }}
    end
  end

  defp parameter_ids_to_bitmasks(parameter_ids) do
    bitmasks =
      for(id <- 1..Enum.max(parameter_ids), do: if(id in parameter_ids, do: 1, else: 0))
      |> Enum.chunk_every(8, 8, [0, 0, 0, 0, 0, 0, 0, 0])
      |> Enum.map(&for bit <- &1, into: <<>>, do: <<bit::size(1)>>)

    for bitmask <- bitmasks, into: <<>>, do: bitmask
  end

  defp bitmasks_to_parameter_names(binary, number_of_parameter_masks) do
    parameter_masks = for <<mask::size(8) <- binary>>, do: <<mask::size(8)>>

    bits =
      parameter_masks
      |> Enum.map(&for <<bit::size(1) <- &1>>, into: [], do: bit)
      |> List.flatten()

    if Enum.count(bits) == number_of_parameter_masks * 8 do
      parameter_ids = for id <- 1..Enum.count(bits), Enum.at(bits, id - 1) != 0, into: [], do: id

      Enum.reduce_while(parameter_ids, {:ok, []}, fn parameter_id, {:ok, acc} ->
        case WindowCovering.decode_parameter_name(parameter_id) do
          {:ok, parameter_name} ->
            {:cont, {:ok, [parameter_name | acc]}}

          {:error, %DecodeError{} = error} ->
            {:halt, {:error, error}}
        end
      end)
    else
      {:error, :invalid_masks}
    end
  end
end