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