lib/grizzly/zwave/commands/multi_channel_aggregated_members_report.ex

defmodule Grizzly.ZWave.Commands.MultiChannelAggregatedMembersReport do
  @moduledoc """
  This command is used to advertise the members of an Aggregated End Point.

  Params:

    * `:aggregated_end_point` - an aggregated end_point (required)

    * `:members` - the lists of end points member of the aggregated end point (required, can be an empty list)

  """

  @behaviour Grizzly.ZWave.Command

  alias Grizzly.ZWave.Command
  alias Grizzly.ZWave.CommandClasses.MultiChannel

  @type param ::
          {:aggregated_end_point, MultiChannel.end_point()}
          | {:members, [MultiChannel.end_point()]}

  @impl true
  def new(params) do
    command = %Command{
      name: :multi_channel_aggregated_members_report,
      command_byte: 0x0F,
      command_class: MultiChannel,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl true
  def encode_params(command) do
    aggregated_end_point = Command.param!(command, :aggregated_end_point)
    members = Command.param!(command, :members)
    encoded_members = encode_members(members)
    count = byte_size(encoded_members)
    <<0x00::size(1), aggregated_end_point::size(7), count>> <> encoded_members
  end

  @impl true
  def decode_params(
        <<0x00::size(1), aggregated_end_point::size(7), count, bitmasks::binary-size(count)>>
      ) do
    members = decode_members(bitmasks)
    {:ok, [aggregated_end_point: aggregated_end_point, members: members]}
  end

  defp encode_members(members) do
    masks_count = ceil(Enum.max(members) / 8)

    for i <- 0..(masks_count - 1), into: <<>> do
      start = i * 8 + 1

      for j <- (start + 7)..start, into: <<>> do
        if j in members, do: <<0x01::size(1)>>, else: <<0x00::size(1)>>
      end
    end
  end

  defp decode_members(bitmasks) do
    masks = for byte <- :erlang.binary_to_list(bitmasks), do: <<byte>>

    for i <- 0..(Enum.count(masks) - 1) do
      mask = Enum.at(masks, i)
      indexed_bits = for(<<x::1 <- mask>>, do: x) |> Enum.reverse() |> Enum.with_index(1)
      start = i * 8

      Enum.reduce(indexed_bits, [], fn {bit, index}, acc ->
        if bit == 1, do: [index + start | acc], else: acc
      end)
    end
    |> List.flatten()
    |> Enum.sort()
  end
end