Packages

BACstack is a low-level Elixir implementation for the ASHRAE standard 135, BACnet - Building Automation and Controller network. In its current published state, v0.0.x, it can (only) act as a BACnet client for other BACnet devices.

Current section

Files

Jump to
bacstack lib bacnet protocol read_access_result.ex
Raw

lib/bacnet/protocol/read_access_result.ex

defmodule BACnet.Protocol.ReadAccessResult do
  @moduledoc """
  A Read Access Result is the response data returned for one object when a
  Read Property Multiple request is processed. It contains the Object
  Identifier of the object that was read, followed by a list of individual
  property results (which may contain either a successful value or an error
  for that specific property).

  Because Read Property Multiple can request many properties from many objects
  in one call, the response must be able to report partial success: some
  properties may be returned successfully while others may fail with individual
  errors (for example, a property that does not exist or is currently
  inaccessible). The Read Access Result structure makes this per-property
  error reporting possible.

  This type is only used in the response path of Read Property Multiple; the
  request side uses Access Specification instead.
  """

  # TODO: Throw argument error in encode if not valid

  alias BACnet.Protocol.ApplicationTags
  alias BACnet.Protocol.ObjectIdentifier
  alias BACnet.Protocol.ReadAccessResult.ReadResult

  import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4]

  @typedoc """
  The result of a Read Property Multiple request for one object.

  Contains the object identifier and the list of per-property ReadResult entries (each either a
  successful value or a BACnet error for that property).
  """
  @type t :: %__MODULE__{
          object_identifier: ObjectIdentifier.t(),
          results: [ReadResult.t()]
        }

  @fields [
    :object_identifier,
    :results
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a BACnet Read Access Result into BACnet application tags encoding.
  """
  @spec encode(t(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{} = result, _opts \\ []) do
    with {:ok, obj, _header} <-
           ApplicationTags.encode_value({:object_identifier, result.object_identifier}),
         {:ok, properties} <- encode_result_list(result.results) do
      result_list =
        if properties != [] do
          [{:constructed, {1, properties, 0}}]
        else
          []
        end

      parameters = [
        {:tagged, {0, obj, byte_size(obj)}}
        | result_list
      ]

      {:ok, parameters}
    end
  end

  @doc """
  Parses a BACnet Read Access Result from BACnet application tags encoding.
  """
  @spec parse(ApplicationTags.encoding_list()) ::
          {:ok, {t(), rest :: ApplicationTags.encoding_list()}}
          | {:error, term()}
  def parse(tags) when is_list(tags) do
    with {:ok, obj, rest} <-
           pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :object_identifier, false),
         {:ok, results_raw, rest} <-
           pattern_extract_tags(rest, {:constructed, {1, _t, _l}}, nil, true),
         {:ok, results} <- parse_result_list(results_raw) do
      result = %__MODULE__{
        object_identifier: obj,
        results: results
      }

      {:ok, {result, rest}}
    else
      {:error, :missing_pattern} -> {:error, :invalid_tags}
      {:error, _err} = err -> err
    end
  end

  @doc """
  Validates whether the given read access result is in form valid.

  It only validates the struct is valid as per type specification.
  """
  @spec valid?(t()) :: boolean()
  def valid?(
        %__MODULE__{
          object_identifier: %ObjectIdentifier{} = obj,
          results: results
        } = _t
      )
      when is_list(results) do
    ObjectIdentifier.valid?(obj) and
      Enum.all?(results, fn
        %ReadResult{} = prop -> ReadResult.valid?(prop)
        _else -> false
      end)
  end

  def valid?(%__MODULE__{} = _t), do: false

  defp encode_result_list([]), do: {:ok, []}

  defp encode_result_list(results) do
    result =
      Enum.reduce_while(results, {:ok, []}, fn item, {:ok, acc} ->
        case ReadResult.encode(item) do
          {:ok, list} -> {:cont, {:ok, [list | acc]}}
          term -> {:halt, term}
        end
      end)

    case result do
      {:ok, list} ->
        new_list =
          list
          |> Enum.reverse()
          |> List.flatten()

        {:ok, new_list}

      term ->
        term
    end
  end

  defp parse_result_list(nil), do: {:ok, []}

  defp parse_result_list({:constructed, {1, results, _len}}) do
    result =
      Enum.reduce_while(1..100_000//1, {results, []}, fn
        _iter, {tags, acc} ->
          case ReadResult.parse(tags) do
            {:ok, {item, []}} -> {:halt, {:ok, [item | acc]}}
            {:ok, {item, rest}} -> {:cont, {rest, [item | acc]}}
            term -> {:halt, term}
          end
      end)

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