defmodule BACnet.Protocol.DeviceObjectPropertyRef do @moduledoc """ A Device Object Property Reference is the most complete form of property reference in BACnet. It identifies a specific property (with optional array index) inside a specific object, optionally qualified by the device that contains that object. This type is used whenever a reference must be unambiguous across the entire BACnet internetwork. It appears in many event and fault parameters that monitor remote properties, inside the log device object property list of Trend Log (Multiple) objects, and in the various "reference" properties used by Schedule, Command, and other coordinating objects. When `device_identifier` is nil, the reference is understood to be local to the device that contains the referring object. ### Examples (Doc Test) ```elixir iex> ref = %DeviceObjectPropertyRef{ ...> device_identifier: %ObjectIdentifier{type: :device, instance: 42}, ...> 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 of an object, optionally qualified by the device containing that object. """ @type t :: %__MODULE__{ object_identifier: ObjectIdentifier.t(), property_identifier: Constants.property_identifier() | non_neg_integer(), property_array_index: non_neg_integer() | nil, device_identifier: ObjectIdentifier.t() | nil } @fields [ :object_identifier, :property_identifier, :property_array_index, :device_identifier ] @enforce_keys @fields defstruct @fields @doc """ Encodes a BACnet device 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), {:ok, devident, _header} <- (if ref.device_identifier do ApplicationTags.encode_value({:object_identifier, ref.device_identifier}, 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 || <<>>)}, tagged: {3, devident, byte_size(devident || <<>>)} ] {:ok, Enum.filter(base, fn {_type, {_t, con, _l}} -> con end)} else {:error, _err} = err -> err end end @doc """ Parses a BACnet device 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, ref} <- pattern_extract_tags(ref, {:tagged, {2, _t, _l}}, :unsigned_integer, true), {:ok, device_identifier, rest} <- pattern_extract_tags(ref, {:tagged, {3, _t, _l}}, :object_identifier, 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, device_identifier: device_identifier } {:ok, {objref, rest}} else {:error, :missing_pattern} -> {:error, :invalid_tags} {:error, _err} = err -> err end end @doc """ Validates whether the given device 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, device_identifier: dev_ref } = _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)) and (is_nil(dev_ref) or (is_struct(dev_ref, ObjectIdentifier) and ObjectIdentifier.valid?(dev_ref) and dev_ref.type == :device)) end def valid?(%__MODULE__{} = _t), do: false end