lib/grizzly/zwave/commands/version_report.ex

defmodule Grizzly.ZWave.Commands.VersionReport do
  @moduledoc """
  This module implements command VERSION_REPORT of command class
  COMMAND_CLASS_VERSION

  Params:

    * `:library_type` - The type of Z-Wave device library is running (required)
    * `:protocol_version` - The "version.sub" of the implemented Z-Wave protocol
      (required)
    * `:firmware_version` - The "version.sub" of the firmware (required)
    * `:hardware_version` - the hardware version - (optional V1, required V2
      and above)
    * `:other_firmware_versions` -  The list of "version.sub" of the other
      firmware targets (optional V1, required V2 and above)

  """

  @behaviour Grizzly.ZWave.Command

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

  @type library_type ::
          :static_controller
          | :controller
          | :enhanced_slave
          | :slave
          | :installer
          | :routing_slave
          | :bridge_controller
          | :device_under_test
          | :av_remote
          | :av_device
  @type protocol_version :: String.t()
  @type firmware_version :: String.t()
  @type param ::
          {:library_type, library_type}
          | {:protocol_version, protocol_version}
          | {:firmware_version, firmware_version}
          | {:hardware_version, non_neg_integer}
          | {:other_firmware_versions, [firmware_version]}

  @impl true
  def new(params) do
    command = %Command{
      name: :version_report,
      command_byte: 0x12,
      command_class: Version,
      params: params,
      impl: __MODULE__
    }

    {:ok, command}
  end

  @impl true
  # Version 1
  def decode_params(
        <<library_type, protocol_version, protocol_sub_version, firmware_version,
          firmware_sub_version>>
      ) do
    with {:ok, library_type} <- decode_library_type(library_type) do
      {:ok,
       [
         library_type: library_type,
         protocol_version: "#{protocol_version}.#{protocol_sub_version}",
         firmware_version: "#{firmware_version}.#{firmware_sub_version}"
       ]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  # Version 2
  def decode_params(
        <<library_type, protocol_version, protocol_sub_version, firmware_version,
          firmware_sub_version, hardware_version, firmware_targets,
          other_firmware_version_data::size(firmware_targets)-binary-unit(16)>>
      ) do
    with {:ok, library_type} <- decode_library_type(library_type) do
      other_firmware_versions = for <<v::8, s::8 <- other_firmware_version_data>>, do: "#{v}.#{s}"

      {:ok,
       [
         library_type: library_type,
         protocol_version: "#{protocol_version}.#{protocol_sub_version}",
         firmware_version: "#{firmware_version}.#{firmware_sub_version}",
         hardware_version: hardware_version,
         other_firmware_versions: other_firmware_versions
       ]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  # Version 3 - patch for zipgateway 7.14.2 and 7.15
  def decode_params(
        <<library_type, protocol_version, protocol_sub_version, firmware_version,
          firmware_sub_version, hardware_version, firmware_targets,
          other_firmware_version_data::size(2)-binary-unit(16)>>
      )
      when firmware_targets <= 1 do
    with {:ok, library_type} <- decode_library_type(library_type) do
      other_firmware_versions = for <<v::8, s::8 <- other_firmware_version_data>>, do: "#{v}.#{s}"

      {:ok,
       [
         library_type: library_type,
         protocol_version: "#{protocol_version}.#{protocol_sub_version}",
         firmware_version: "#{firmware_version}.#{firmware_sub_version}",
         hardware_version: hardware_version,
         other_firmware_versions: other_firmware_versions
       ]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  # Safe fallback
  def decode_params(
        <<library_type, protocol_version, protocol_sub_version, firmware_version,
          firmware_sub_version, _ignore::binary>>
      ) do
    with {:ok, library_type} <- decode_library_type(library_type) do
      {:ok,
       [
         library_type: library_type,
         protocol_version: "#{protocol_version}.#{protocol_sub_version}",
         firmware_version: "#{firmware_version}.#{firmware_sub_version}"
       ]}
    else
      {:error, %DecodeError{}} = error ->
        error
    end
  end

  @impl true
  def encode_params(command) do
    library_type = Command.param!(command, :library_type)
    protocol_version = Command.param!(command, :protocol_version)
    firmware_version = Command.param!(command, :firmware_version)
    library_type_byte = encode_library_type(library_type)
    {protocol_v, protocol_s} = split_version(protocol_version)
    {firmware_v, firmware_s} = split_version(firmware_version)
    hardware_version = Command.param(command, :hardware_version)

    if hardware_version == nil do
      <<library_type_byte, protocol_v, protocol_s, firmware_v, firmware_s>>
    else
      other_firmware_versions = Command.param!(command, :other_firmware_versions)
      number_of_firmware_targets = Enum.count(other_firmware_versions)
      other_firmware_version_data = encode_other_firmware_versions(other_firmware_versions)

      <<library_type_byte, protocol_v, protocol_s, firmware_v, firmware_s, hardware_version,
        number_of_firmware_targets, other_firmware_version_data::binary>>
    end
  end

  defp decode_library_type(0x01), do: {:ok, :static_controller}
  defp decode_library_type(0x02), do: {:ok, :controller}
  defp decode_library_type(0x03), do: {:ok, :enhanced_slave}
  defp decode_library_type(0x04), do: {:ok, :slave}
  defp decode_library_type(0x05), do: {:ok, :installer}
  defp decode_library_type(0x06), do: {:ok, :routing_slave}
  defp decode_library_type(0x07), do: {:ok, :bridge_controller}
  defp decode_library_type(0x08), do: {:ok, :device_under_test}
  defp decode_library_type(0x0A), do: {:ok, :av_remote}
  defp decode_library_type(0x0B), do: {:ok, :av_device}

  defp decode_library_type(byte),
    do:
      {:error,
       %DecodeError{
         value: byte,
         param: :library_type,
         command: :version_report
       }}

  defp encode_library_type(:static_controller), do: 0x01
  defp encode_library_type(:controller), do: 0x02
  defp encode_library_type(:enhanced_slave), do: 0x03
  defp encode_library_type(:slave), do: 0x04
  defp encode_library_type(:installer), do: 0x05
  defp encode_library_type(:routing_slave), do: 0x06
  defp encode_library_type(:bridge_controller), do: 0x07
  defp encode_library_type(:device_under_test), do: 0x08
  defp encode_library_type(:av_remote), do: 0x0A
  defp encode_library_type(:av_device), do: 0x0B

  defp encode_other_firmware_versions(other_firmware_versions) do
    for other_firmware_version <- other_firmware_versions, into: <<>> do
      {v, s} = split_version(other_firmware_version)
      <<v, s>>
    end
  end

  defp split_version(version_with_sub) do
    [v, s] = String.split(version_with_sub, ".")
    {v_byte, ""} = Integer.parse(v)
    {s_byte, ""} = Integer.parse(s)
    {v_byte, s_byte}
  end
end