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

lib/bacnet/protocol/object_property_ref.ex

defmodule BACnet.Protocol.ObjectPropertyRef do
  @moduledoc """
  An Object Property Reference is the most common way to name a specific property
  of a specific object in BACnet. It consists of an Object Identifier, a Property
  Identifier (which may be a standard named property or a vendor-specific numeric
  identifier), and an optional array index when the property is an array.

  This type is used everywhere a service or object needs to refer to a property
  that may live in another object or even another device: in Read Property and
  Write Property requests, inside the monitored values of COV subscriptions,
  in the log device object property list of Trend Log Multiple objects, in
  event parameters that reference other properties for comparison, and in many
  more places.

  ### Examples (Doc Test)

  ```elixir
  iex> ref = %ObjectPropertyRef{
  ...>   object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1},
  ...>   property_identifier: :present_value,
  ...>   property_array_index: nil
  ...> }
  iex> ref.property_identifier
  :present_value
  ```
  """

  # TODO: Throw argument error in encode if not valid

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

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

  @typedoc """
  References a specific property, optionally an array element, of an object.
  """
  @type t :: %__MODULE__{
          object_identifier: ObjectIdentifier.t(),
          property_identifier: Constants.property_identifier() | non_neg_integer(),
          property_array_index: non_neg_integer() | nil
        }

  @fields [
    :object_identifier,
    :property_identifier,
    :property_array_index
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a BACnet object property reference into application tags encoding.
  """
  @spec encode(t(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{} = ref, opts \\ []) do
    with {:ok, objident, _header} <-
           ApplicationTags.encode_value({:object_identifier, ref.object_identifier}, opts),
         {:ok, propident, _header} <-
           ApplicationTags.encode_value(
             {:enumerated, Constants.by_name_atom(:property_identifier, ref.property_identifier)},
             opts
           ),
         {:ok, propindex, _header} <-
           (if ref.property_array_index do
              ApplicationTags.encode_value({:unsigned_integer, ref.property_array_index}, opts)
            else
              {:ok, nil, nil}
            end) do
      base = [
        tagged: {0, objident, byte_size(objident)},
        tagged: {1, propident, byte_size(propident)},
        tagged: {2, propindex, byte_size(propindex || <<>>)}
      ]

      {:ok, Enum.filter(base, fn {_type, {_t, con, _l}} -> con end)}
    else
      {:error, _err} = err -> err
    end
  end

  @doc """
  Parses a BACnet object property reference 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, object_identifier, ref} <-
           pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :object_identifier, false),
         {:ok, property_identifier, ref} <-
           pattern_extract_tags(ref, {:tagged, {1, _t, _l}}, :enumerated, false),
         {:ok, prop_array_index, rest} <-
           pattern_extract_tags(ref, {:tagged, {2, _t, _l}}, :unsigned_integer, true) do
      objref = %__MODULE__{
        object_identifier: object_identifier,
        property_identifier:
          Constants.by_value(:property_identifier, property_identifier, property_identifier),
        property_array_index: prop_array_index
      }

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

  @doc """
  Validates whether the given object property reference 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_ref,
          property_identifier: prop_identifier,
          property_array_index: array_index
        } = _t
      ) do
    ObjectIdentifier.valid?(obj_ref) and
      (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))
  end

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