lib/grizzly/zwave/commands/association_group_info_report.ex

defmodule Grizzly.ZWave.Commands.AssociationGroupInfoReport do
  @moduledoc """
  This command is used to advertise the properties of one or more association
  groups.

  Params:

    * `:dynamic` - whether the group info is subject to be changed by the device
    * `:groups_info` - a list of group info
    * `:list_mode` - a boolean if the report should be in list mode. In some
      cases this needs to be forced, like if you are reporting one group but the
      get query requested list mode. If you are reporting more than one group
      then this parameter is optional as it will know that more than one group
      can only be reported with list mode `true`. If you don't set this option
      to `true` when reporting one group then this will default to `false`.
  """

  @behaviour Grizzly.ZWave.Command

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

  @type group_info() :: [group_id: byte(), profile: atom()]
  @type param() ::
          {:groups_info, [group_info()]} | {:dynamic, boolean() | {:list_mode, boolean()}}

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

    {:ok, command}
  end

  @impl Command
  def encode_params(command) do
    list_mode = get_list_mode(command)
    dynamic? = Command.param!(command, :dynamic)
    dynamic_bit = if dynamic?, do: 0x01, else: 0x00
    groups_info = Command.param!(command, :groups_info)
    group_count = Enum.count(groups_info)

    encoded_groups_info =
      for group_info <- groups_info, into: <<>> do
        group_id = Keyword.fetch!(group_info, :group_id)
        profile = Keyword.fetch!(group_info, :profile)
        <<group_id, 0x00>> <> encode_profile(Atom.to_string(profile)) <> <<0x00, 0x00, 0x00>>
      end

    <<list_mode::size(1), dynamic_bit::size(1), group_count::size(6)>> <> encoded_groups_info
  end

  @impl Command
  def decode_params(
        <<list_mode::size(1), dynamic_bit::size(1), group_count::size(6),
          encoded_groups_info::binary>>
      ) do
    dynamic? = dynamic_bit == 0x01
    list_mode? = list_mode == 0x01

    case decode_groups_info(group_count, encoded_groups_info) do
      {:ok, groups_info} ->
        {:ok, [dynamic: dynamic?, groups_info: groups_info, list_mode: list_mode?]}

      {:error, %DecodeError{} = error} ->
        error
    end
  end

  defp decode_groups_info(group_count, encoded_groups_info) do
    list = :erlang.binary_to_list(encoded_groups_info)
    chunks = Enum.chunk_every(list, 7)

    if Enum.count(chunks) != group_count do
      %DecodeError{}
    else
      result =
        Enum.reduce_while(
          chunks,
          [],
          fn chunk, acc ->
            binary = :erlang.list_to_binary(chunk)

            case binary do
              <<
                group_id,
                0x00,
                profile_msb,
                profile_lsb,
                _reserved,
                _event_code::size(16)
              >> ->
                profile = decode_profile(profile_msb, profile_lsb)
                {:cont, [[group_id: group_id, profile: profile] | acc]}

              _other ->
                {:halt,
                 {:error,
                  %DecodeError{
                    value: binary,
                    param: :groups_info,
                    command: :association_group_info_report
                  }}}
            end
          end
        )

      case result do
        {:error, error} -> {:error, error}
        _groups_info -> {:ok, Enum.reverse(result)}
      end
    end
  end

  defp decode_profile(0x00, 0x00), do: :general_na
  defp decode_profile(0x00, 0x01), do: :general_lifeline
  defp decode_profile(0x20, key), do: :"control_key_#{key}"
  defp decode_profile(0x31, 0x01), do: :sensor_air_temperature
  defp decode_profile(0x31, 0x05), do: :sensor_humidity
  defp decode_profile(0x71, 0x01), do: :notification_smoke_alarm
  defp decode_profile(0x71, 0x03), do: :notification_co2_alarm
  defp decode_profile(0x71, _), do: :notification_unknown
  defp decode_profile(0x32, 0x01), do: :meter_electric_kwh
  defp decode_profile(0x32, 0x02), do: :meter_gas
  defp decode_profile(0x32, 0x03), do: :meter_water
  defp decode_profile(0x32, _), do: :meter_unknown
  defp decode_profile(0x6B, key), do: :"irrigation_channel_#{key}"
  defp decode_profile(_profile_msb, _profile_lsb), do: :unknown

  defp encode_profile("general_na"), do: <<0x00, 0x00>>
  defp encode_profile("general_lifeline"), do: <<0x00, 0x01>>
  defp encode_profile(<<"control_key_", key::binary>>), do: <<0x20, elem(Integer.parse(key), 0)>>
  defp encode_profile("sensor_air_temperature"), do: <<0x31, 0x01>>
  defp encode_profile("sensor_humidity"), do: <<0x31, 0x05>>
  defp encode_profile("notification_smoke_alarm"), do: <<0x71, 0x01>>
  defp encode_profile("notification_co2_alarm"), do: <<0x71, 0x03>>
  defp encode_profile("notification_unknown"), do: <<0x71, 0x00>>
  defp encode_profile("meter_electric_kwh"), do: <<0x32, 0x01>>
  defp encode_profile("meter_gas"), do: <<0x32, 0x02>>
  defp encode_profile("meter_water"), do: <<0x32, 0x03>>
  defp encode_profile("meter_unknown"), do: <<0x32, 0x00>>

  defp encode_profile(<<"irrigation_channel_", key::binary>>),
    do: <<0x6B, elem(Integer.parse(key), 0)>>

  defp get_list_mode(command) do
    if Command.param(command, :list_mode, false) do
      0x01
    else
      groups_info = Command.param!(command, :groups_info)

      if length(groups_info) > 1, do: 0x01, else: 0x00
    end
  end
end