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 property_value.ex
Raw

lib/bacnet/protocol/property_value.ex

defmodule BACnet.Protocol.PropertyValue do
  @moduledoc """
  Property Value packages a property reference (identifier + optional array
  index) together with the value to be written (or that was read). It is the
  basic payload element inside `WritePropertyMultiple`, `ReadPropertyMultiple`
  results, and several action / event parameter structures.

  The optional `priority` field is only meaningful when the target property is
  commandable. If omitted on a write, the device uses the default priority
  defined for that object type (usually 8 for many objects).

  ### BACnet Specification References
  - Used heavily in `WritePropertyMultiple-Request` (15.10) and the corresponding
    ACK / error structures.
  - Also appears inside `BACnet.Protocol.ActionCommand`, `BACnet.Protocol.NotificationParameters`, `BACnet.Protocol.EventParameters`, etc.
  - The `property_value` field is typed as `ABSTRACT-SYNTAX.&Type` in the ASN.1,
    which in this library is represented as `ApplicationTags.Encoding.t()`.

  ### Examples (Doc Test)

  ```elixir
  iex> pv = %PropertyValue{
  ...>   property_identifier: :present_value,
  ...>   property_array_index: nil,
  ...>   property_value: {:real, 42.5},
  ...>   priority: 8
  ...> }
  iex> pv.priority
  8
  ```

  ### See Also
  - `BACnet.Protocol.ActionCommand`
  - `BACnet.Protocol.ApplicationTags.Encoding`
  - `BACnet.Protocol.WritePropertyMultiple`
  """

  # TODO: Throw argument error in encode if not valid

  alias BACnet.Protocol.ApplicationTags
  alias BACnet.Protocol.Constants

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

  @typedoc """
  (property id + optional array index + value + optional priority) tuple used
  in WritePropertyMultiple and several action/event structures.
  """
  @type t :: %__MODULE__{
          property_identifier: Constants.property_identifier() | non_neg_integer(),
          property_array_index: non_neg_integer() | nil,
          property_value: ApplicationTags.Encoding.t() | [ApplicationTags.Encoding.t()],
          priority: 1..16 | nil
        }

  @fields [
    :property_identifier,
    :property_array_index,
    :property_value,
    :priority
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a BACnet property value into application tags encoding.
  """
  @spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{} = property, opts \\ []) do
    with {:ok, propident, _header} <-
           ApplicationTags.encode_value(
             {:enumerated,
              Constants.by_name_atom(:property_identifier, property.property_identifier)},
             opts
           ),
         {:ok, propindex, _header} <-
           (if property.property_array_index do
              ApplicationTags.encode_value(
                {:unsigned_integer, property.property_array_index},
                opts
              )
            else
              {:ok, nil, nil}
            end),
         {:ok, value} <- do_encode_value(property.property_value),
         {:ok, priority, _header} <-
           (if property.priority do
              ApplicationTags.encode_value({:unsigned_integer, property.priority}, opts)
            else
              {:ok, nil, nil}
            end) do
      list =
        Enum.reduce(
          [{3, priority}, {2, value}, {1, propindex}, {0, propident}],
          [],
          fn
            {_tag, nil}, acc ->
              acc

            {2, element}, acc ->
              [{:constructed, {2, element, 0}} | acc]

            {tag, element}, acc ->
              [{:tagged, {tag, element, byte_size(element)}} | acc]
          end
        )

      {:ok, list}
    else
      {:error, _term} = term -> term
    end
  end

  @doc """
  Encode a list of property values into application tag-encoded property values.
  """
  @spec encode_all([t()], Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode_all(properties, opts \\ []) when is_list(properties) do
    result =
      Enum.reduce_while(properties, {:ok, []}, fn
        %__MODULE__{} = prop, {:ok, acc} ->
          case encode(prop, opts) do
            {:ok, enc} -> {:cont, {:ok, [enc | acc]}}
            term -> {:halt, term}
          end

        _prop, _acc ->
          {:halt, {:error, :invalid_list_element}}
      end)

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

        {:ok, new_list}

      term ->
        term
    end
  end

  @doc """
  Parse application tag-encoded property value into a struct.
  """
  @spec parse(ApplicationTags.encoding_list()) ::
          {:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term()}
  def parse(tags) when is_list(tags) do
    with {:ok, property_identifier, prop} <-
           pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :enumerated, false),
         {:ok, prop_array_index, prop} <-
           pattern_extract_tags(prop, {:tagged, {1, _t, _l}}, :unsigned_integer, true),
         {:ok, {:constructed, {2, value, _l}}, prop} <-
           pattern_extract_tags(prop, {:constructed, {2, _t, _l}}, nil, false),
         {:ok, priority, rest} <-
           pattern_extract_tags(prop, {:tagged, {3, _t, _l}}, :unsigned_integer, true) do
      propvalue = %__MODULE__{
        property_identifier:
          Constants.by_value(:property_identifier, property_identifier, property_identifier),
        property_array_index: prop_array_index,
        property_value:
          if(is_list(value),
            do: Enum.map(value, &ApplicationTags.Encoding.create!/1),
            else: ApplicationTags.Encoding.create!(value)
          ),
        priority: priority
      }

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

  @doc """
  Parse application tag-encoded property values into a list of structs.
  """
  @spec parse_all(ApplicationTags.encoding_list()) ::
          {:ok, [t()]} | {:error, term()}
  def parse_all(tags) when is_list(tags) do
    result =
      Enum.reduce_while(1..100_000//1, {tags, []}, fn
        _iter, {tags, acc} ->
          case parse(tags) do
            {:ok, {item, []}} -> {:halt, {:ok, {nil, [item | acc]}}}
            {:ok, {item, rest}} -> {:cont, {rest, [item | acc]}}
            {:error, _err} = err -> {:halt, err}
          end
      end)

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

  @doc """
  Validates whether the given property value is in form valid.

  It only validates the struct is valid as per type specification.
  """
  @spec valid?(t()) :: boolean()
  def valid?(
        %__MODULE__{
          property_identifier: prop_identifier,
          property_array_index: array_index,
          property_value: encoding,
          priority: priority
        } = _t
      )
      when (is_nil(priority) or priority in 1..16) and
             (is_struct(encoding, ApplicationTags.Encoding) or is_list(encoding)) do
    (Constants.has_by_name(:property_identifier, prop_identifier) or
       (is_integer(prop_identifier) and prop_identifier >= 0 and
          prop_identifier <= Constants.macro_by_name(:asn1, :max_instance_and_property_id))) and
      (is_nil(array_index) or (is_integer(array_index) and array_index >= 0)) and
      (is_struct(encoding, ApplicationTags.Encoding) or
         Enum.all?(encoding, &is_struct(&1, ApplicationTags.Encoding)))
  end

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

  @spec do_encode_value(ApplicationTags.Encoding.t() | [ApplicationTags.Encoding.t()]) ::
          {:ok, term()} | {:error, term()}
  defp do_encode_value(%ApplicationTags.Encoding{} = encoding),
    do: ApplicationTags.Encoding.to_encoding(encoding)

  defp do_encode_value(encoding) when is_list(encoding) do
    result =
      Enum.reduce_while(encoding, {:ok, []}, fn item, {:ok, acc} ->
        case ApplicationTags.Encoding.to_encoding(item) do
          {:ok, enc} -> {:cont, {:ok, [enc | acc]}}
          other -> {:halt, other}
        end
      end)

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