lib/grizzly/zwave/command_classes/network_management_installation_maintenance.ex

defmodule Grizzly.ZWave.CommandClasses.NetworkManagementInstallationMaintenance do
  @moduledoc """
  "NetworkManagementInstallationMaintenance" Command Class

  The Network Management Installation and Maintenance Command Class is used to access statistical
  data.
  """

  use Bitwise, only_operators: true

  @type route_type ::
          :no_route | :last_working_route | :next_to_last_working_route | :set_by_application
  @type speeds :: [speed()]
  @type speed :: :"9.6kbit/s" | :"40kbit/s" | :"100kbit/s" | :reserved
  @type statistics :: [statistic]
  @type statistic ::
          {:route_changes, byte}
          | {:transmission_count, byte}
          | {:neighbors, [neighbor]}
          | {:packet_error_count, byte}
          | {:sum_of_transmission_times, non_neg_integer}
          | {:sum_of_transmission_times_squared, non_neg_integer}
  @type neighbor :: [neighbor_param]
  @type neighbor_param ::
          {:node_id, byte}
          | {:repeater?, boolean}
          | {:speed, speeds}
  @type rssi ::
          :rssi_not_available | :rssi_max_power_saturated | :rssi_below_sensitivity | -94..-32

  alias Grizzly.ZWave.DecodeError
  @behaviour Grizzly.ZWave.CommandClass

  @impl true
  def byte(), do: 0x67

  @impl true
  def name(), do: :network_management_installation_maintenance

  @spec route_type_to_byte(route_type) :: byte
  def route_type_to_byte(type) do
    case type do
      :no_route -> 0x00
      :last_working_route -> 0x01
      :next_to_last_working_route -> 0x02
      :set_by_application -> 0x10
    end
  end

  @spec route_type_from_byte(any) :: {:error, Grizzly.ZWave.DecodeError.t()} | {:ok, route_type}
  def route_type_from_byte(byte) do
    case byte do
      0x00 -> {:ok, :no_route}
      0x01 -> {:ok, :last_working_route}
      0x02 -> {:ok, :next_to_last_working_route}
      0x10 -> {:ok, :set_by_application}
      byte -> {:error, %DecodeError{param: :type, value: byte}}
    end
  end

  @spec speeds_to_byte(speeds()) :: byte()
  def speeds_to_byte(speeds) do
    byte = if :"9.6kbit/s" in speeds, do: 0x01, else: 0x00
    byte = if :"40kbit/s" in speeds, do: byte ||| 0x02, else: byte
    if :"100kbit/s" in speeds, do: byte ||| 0x04, else: byte
  end

  @spec speeds_from_byte(byte) :: {:ok, speeds}
  def speeds_from_byte(byte) do
    case byte do
      0x01 -> {:ok, [:"9.6kbit/s"]}
      0x02 -> {:ok, [:"40kbit/s"]}
      0x03 -> {:ok, [:"9.6kbit/s", :"40kbit/s"]}
      0x04 -> {:ok, [:"100kbit/s"]}
      0x05 -> {:ok, [:"9.6kbit/s", :"100kbit/s"]}
      0x06 -> {:ok, [:"40kbit/s", :"100kbit/s"]}
      0x07 -> {:ok, [:"9.6kbit/s", :"40kbit/s", :"100kbit/s"]}
      _other -> {:ok, []}
    end
  end

  @spec rssi_to_byte(rssi) :: byte
  def rssi_to_byte(:rssi_below_sensitivity), do: 0x7D
  def rssi_to_byte(:rssi_max_power_saturated), do: 0x7E
  def rssi_to_byte(:rssi_not_available), do: 0x7F
  def rssi_to_byte(value) when value in -94..-32, do: 256 + value

  @spec rssi_from_byte(byte) :: {:ok, rssi} | {:error, DecodeError.t()}
  def rssi_from_byte(0x7D), do: {:ok, :rssi_below_sensitivity}
  def rssi_from_byte(0x7E), do: {:ok, :rssi_max_power_saturated}
  def rssi_from_byte(0x7F), do: {:ok, :rssi_not_available}
  def rssi_from_byte(byte) when byte in 0xE0..0xA2, do: {:ok, byte - 256}
  def rssi_from_byte(byte), do: {:error, %DecodeError{value: byte}}

  def repeaters_to_bytes(repeaters) do
    full_repeaters = (repeaters ++ [0, 0, 0, 0]) |> Enum.take(4)
    for repeater <- full_repeaters, into: <<>>, do: <<repeater>>
  end

  def repeaters_from_bytes(bytes) do
    :erlang.binary_to_list(bytes)
    |> Enum.reject(&(&1 == 0))
  end

  def statistics_to_binary(statistics) do
    for statistic <- statistics, into: <<>> do
      case statistic do
        {:route_changes, byte} ->
          <<0x00, 0x01, byte>>

        {:transmission_count, byte} ->
          <<0x01, 0x01, byte>>

        {:neighbors, neighbors} ->
          binary = neighbors_to_binary(neighbors)
          <<0x02, byte_size(binary)>> <> binary

        {:packet_error_count, byte} ->
          <<0x03, 0x01, byte>>

        {:sum_of_transmission_times, sum} ->
          <<0x04, 0x04, sum::integer-unsigned-unit(8)-size(4)>>

        {:sum_of_transmission_times_squared, sum} ->
          <<0x05, 0x04, sum::integer-unsigned-unit(8)-size(4)>>
      end
    end
  end

  def statistics_from_binary(<<>>) do
    {:ok, []}
  end

  def statistics_from_binary(<<0x00, 0x01, byte, rest::binary>>) do
    with {:ok, other_statistics} <- statistics_from_binary(rest) do
      {:ok, [route_changes: byte] ++ other_statistics}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  def statistics_from_binary(<<0x01, 0x01, byte, rest::binary>>) do
    with {:ok, other_statistics} <- statistics_from_binary(rest) do
      {:ok, [transmission_count: byte] ++ other_statistics}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  def statistics_from_binary(
        <<0x02, length, neighbors_binary::binary-size(length), rest::binary>>
      ) do
    with {:ok, other_statistics} <- statistics_from_binary(rest),
         {:ok, neighbors} <- neighbors_from_binary(neighbors_binary) do
      {:ok, [neighbors: neighbors] ++ other_statistics}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  def statistics_from_binary(<<0x03, 0x01, byte, rest::binary>>) do
    with {:ok, other_statistics} <- statistics_from_binary(rest) do
      {:ok, [packet_error_count: byte] ++ other_statistics}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  def statistics_from_binary(<<0x04, 0x04, sum::integer-unsigned-unit(8)-size(4), rest::binary>>) do
    with {:ok, other_statistics} <- statistics_from_binary(rest) do
      {:ok, [sum_of_transmission_times: sum] ++ other_statistics}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  def statistics_from_binary(<<0x05, 0x04, sum::integer-unsigned-unit(8)-size(4), rest::binary>>) do
    with {:ok, other_statistics} <- statistics_from_binary(rest) do
      {:ok, [sum_of_transmission_times_squared: sum] ++ other_statistics}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  defp neighbors_to_binary(neighbors) do
    for neighbor <- neighbors, into: <<>>, do: neighbor_to_binary(neighbor)
  end

  defp neighbor_to_binary(neighbor) do
    node_id = Keyword.get(neighbor, :node_id)
    repeater_bit = if Keyword.get(neighbor, :repeater?), do: 0x01, else: 0x00
    speeds_bits = Keyword.get(neighbor, :speed) |> speeds_to_byte()
    <<node_id, repeater_bit::size(1), 0x00::size(2), speeds_bits::size(5)>>
  end

  defp neighbors_from_binary(<<>>), do: {:ok, []}

  defp neighbors_from_binary(
         <<node_id, repeater_bit::size(1), _reserved::size(2), speeds_bits::size(5),
           rest::binary>>
       ) do
    with {:ok, speeds} <- speeds_from_byte(speeds_bits),
         {:ok, other_neighbors} <- neighbors_from_binary(rest) do
      neighbor = [node_id: node_id, repeater?: repeater_bit == 1, speed: speeds]
      {:ok, [neighbor | other_neighbors]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end
end