lib/grizzly/zwave/qr_code.ex

defmodule Grizzly.ZWave.QRCode do
  @moduledoc """
  Z-Wave QR code
  This module handles Z-Wave QR codes that follow Silicon Labs
  Software Design Specification SDS13937 and SDS13944.
  """

  alias Grizzly.ZWave
  alias Grizzly.ZWave.{DSK, Security}
  alias Grizzly.ZWave.SmartStart.MetaExtension.UUID16

  @typedoc "QR Code version (S2-only or Smart Start-enabled)"
  @type version() :: :s2 | :smart_start

  @typedoc """
  Device information
  * `:zwave_device_type` - either {generic_device_type, specific_device_type} or a number
  """
  @type t() :: %__MODULE__{
          version: version(),
          requested_keys: [ZWave.Security.key()],
          dsk: ZWave.DSK.t(),
          zwave_device_type: {atom(), atom()} | 0..65535,
          zwave_installer_icon: ZWave.IconType.name() | ZWave.IconType.value(),
          manufacturer_id: 0..65535,
          product_type: 0..65535,
          product_id: 0..65535,
          application_version: {byte(), byte()} | nil,
          uuid16: nil | UUID16.t(),
          supported_protocols: any()
        }

  defstruct version: :smart_start,
            requested_keys: [],
            dsk: ZWave.DSK.zeros(),
            zwave_device_type: 0,
            zwave_installer_icon: 0,
            manufacturer_id: 0,
            product_type: 0,
            product_id: 0,
            application_version: nil,
            uuid16: nil,
            supported_protocols: []

  @lead_in "90"

  @spec decode(<<_::16, _::_*8>>) :: {:ok, t()} | {:error, :invalid_dsk}
  def decode(@lead_in <> binary) do
    {version, binary} = String.split_at(binary, 2)
    {_checksum, binary} = String.split_at(binary, 5)
    {requested_keys, binary} = String.split_at(binary, 3)
    {dsk, binary} = String.split_at(binary, 40)

    requested_keys = requested_keys |> String.to_integer() |> Security.byte_to_keys()

    case DSK.parse(dsk) do
      {:ok, dsk} ->
        tlv = decode_tlv(binary)

        app_version =
          if is_list(tlv[:product_id]) do
            {tlv[:product_id][:app_version], tlv[:product_id][:app_sub_version]}
          else
            nil
          end

        {:ok,
         %__MODULE__{
           version: decode_version(version),
           requested_keys: requested_keys,
           dsk: dsk,
           zwave_device_type:
             {tlv[:product_type][:generic_device_class],
              tlv[:product_type][:specific_device_class]},
           zwave_installer_icon: tlv[:product_type][:installer_icon],
           manufacturer_id: tlv[:product_id][:manufacturer_id],
           product_type: tlv[:product_id][:product_type],
           product_id: tlv[:product_id][:product_id],
           application_version: app_version,
           uuid16: tlv[:uuid_16],
           supported_protocols: tlv[:supported_protocols] || []
         }}

      {:error, _} ->
        {:error, :invalid_dsk}
    end
  end

  defp decode_tlv(binary, acc \\ [])
  defp decode_tlv("", acc), do: acc

  defp decode_tlv(binary, acc) do
    {tag_critical, binary} = String.split_at(binary, 2)
    <<tag::7, _critical::1>> = <<String.to_integer(tag_critical)::8>>

    # critical = if(critical == 1, do: true, else: false)
    {length, binary} = String.split_at(binary, 2)
    length = String.to_integer(length)
    {value, binary} = String.split_at(binary, length)

    tag = decode_tag(tag)

    decode_tlv(binary, [{tag, decode_value(tag, value)} | acc])
  end

  defp decode_tag(0x00), do: :product_type
  defp decode_tag(0x01), do: :product_id
  defp decode_tag(0x02), do: :max_inclusion_request_interval
  defp decode_tag(0x03), do: :uuid_16
  defp decode_tag(0x04), do: :supported_protocols
  defp decode_tag(0x32), do: :name
  defp decode_tag(0x33), do: :location
  defp decode_tag(0x34), do: :smartstart_inclusion_setting
  defp decode_tag(0x35), do: :advanced_joinin
  defp decode_tag(0x36), do: :bootstrapping_mode
  defp decode_tag(0x37), do: :network_status

  defp decode_value(:product_type, value) do
    {classes, installer_icon} = String.split_at(value, 5)
    <<generic::8, specific::8>> = <<String.to_integer(classes)::16>>
    installer_icon = String.to_integer(installer_icon)

    {:ok, generic_device_class} = ZWave.DeviceClasses.generic_device_class_from_byte(generic)

    {:ok, specific_device_class} =
      ZWave.DeviceClasses.specific_device_class_from_byte(
        generic_device_class,
        specific
      )

    {:ok, installer_icon} = ZWave.IconType.to_name(installer_icon)

    %{
      generic_device_class: generic_device_class,
      specific_device_class: specific_device_class,
      installer_icon: installer_icon
    }
  end

  defp decode_value(:product_id, value) do
    [manufacturer_id, product_type, product_id, vsn] =
      value
      |> String.to_charlist()
      |> Enum.chunk_every(5)
      |> Enum.map(&String.to_integer(to_string(&1)))

    <<app_version::8, app_sub_version::8>> = <<vsn::16>>

    [
      manufacturer_id: manufacturer_id,
      product_type: product_type,
      product_id: product_id,
      app_version: app_version,
      app_sub_version: app_sub_version
    ]
  end

  defp decode_value(:uuid_16, value) do
    <<_presentation::2-bytes, decimalized_uuid::binary>> = value

    ints =
      decimalized_uuid
      |> String.to_charlist()
      |> Enum.chunk_every(5)
      |> Enum.map(&String.to_integer(to_string(&1)))

    uuid_string =
      for int <- ints, into: <<>> do
        <<int::16>>
      end

    {:ok, uuid} = Base.encode16(uuid_string) |> UUID16.new(:hex)

    uuid
  end

  defp decode_value(:supported_protocols, value) do
    {int_val, ""} = Integer.parse(value)

    <<_reserved::6, zwave_lr::1, zwave::1, _rest::binary>> = <<int_val>>
    protocols = if(zwave_lr == 1, do: [:zwave_lr], else: [])
    if(zwave == 1, do: [:zwave | protocols], else: protocols)
  end

  defp decode_value(_, value), do: value

  @doc """
  Encode device information into Z-Wave QR Code format
  The output of this is either a 90 byte or 136 byte string that should be put
  into a QR Code. Z-Wave specifies that the 90-byte code (no UUID16) is made
  into a 29x29 pixel QR Code. The 136-byte* code (w/ UUID16) should be put into
  a 33x33 pixel QR Code.
  QR Codes should be encoded as type "text" with error correction "L".
  NOTE: SDS13937 has a mistake with the code length. It says 134 bytes, but the
  UUID16 encode has 2 extra bytes for presentation, so it should be 136.
  """
  @spec encode!(t()) :: iolist()
  def encode!(info) do
    payload = [
      encode_requested_keys(info.requested_keys),
      encode_dsk(info),
      encode_qr_product_type(info),
      encode_qr_product_id(info),
      encode_uuid16(info.uuid16)
    ]

    [@lead_in, encode_version(info.version), checksum(payload), payload]
  end

  defp checksum(payload) do
    <<two_bytes::16, _rest::144>> = :crypto.hash(:sha, payload)
    int_to_string(two_bytes, 5)
  end

  defp encode_version(:s2), do: "00"
  defp encode_version(:smart_start), do: "01"

  defp decode_version("00"), do: :s2
  defp decode_version("01"), do: :smart_start

  defp encode_requested_keys(requested_keys) do
    ZWave.Security.keys_to_byte(requested_keys)
    |> int_to_string(3)
  end

  defp encode_dsk(info), do: ZWave.DSK.to_string(info.dsk, delimiter: "")

  defp encode_qr_product_type(info) do
    # QR Product type = TLV type 00, length 10
    [
      "0010",
      encode_device_type(info.zwave_device_type),
      encode_icon_type(info.zwave_installer_icon)
    ]
  end

  defp encode_device_type({generic, specific}) do
    device_type =
      ZWave.DeviceClasses.generic_device_class_to_byte(generic) * 256 +
        ZWave.DeviceClasses.specific_device_class_to_byte(generic, specific)

    encode_device_type(device_type)
  end

  defp encode_device_type(device_type) when is_integer(device_type) do
    int_to_string(device_type, 5)
  end

  defp encode_icon_type(type) when is_atom(type) do
    {:ok, value} = ZWave.IconType.to_value(type)
    encode_icon_type(value)
  end

  defp encode_icon_type(type), do: int_to_string(type, 5)

  defp encode_qr_product_id(info) do
    app_version =
      cond do
        is_tuple(info.application_version) && tuple_size(info.application_version) == 2 ->
          {app_version, app_sub_version} = info.application_version
          <<vsn::16>> = <<app_version, app_sub_version>>
          int_to_string(vsn, 5)

        is_integer(info.application_version) ->
          int_to_string(info.application_version, 5)

        true ->
          int_to_string(0, 5)
      end

    # QR Product ID = TLV type 02, length 20
    [
      "0220",
      int_to_string(info.manufacturer_id, 5),
      int_to_string(info.product_type, 5),
      int_to_string(info.product_id, 5),
      app_version
    ]
  end

  defp encode_uuid16(nil), do: []

  defp encode_uuid16(uuid16) do
    value = UUID16.encode(uuid16)
    <<0x06, 0x11, presentation, uuid::binary>> = value

    decimalized_uuid =
      uuid
      |> ZWave.DSK.new()
      |> ZWave.DSK.to_string(delimiter: "")

    # UUID16 = TLV type 06, length 42
    ["0642", int_to_string(presentation, 2), decimalized_uuid]
  end

  defp int_to_string(value, num_digits) when value >= 0 do
    value
    |> Integer.to_string(10)
    |> String.pad_leading(num_digits, "0")
  end
end