defmodule BACnet.Protocol.DeviceObjectRef do @moduledoc """ A DeviceObjectRef points to an object, optionally qualified by the device that contains it. It is one of the most common reference types in the entire BACnet object model. - When `device_identifier` is present -> absolute reference (works across the BACnet internetwork). - When `device_identifier` is `nil` -> the reference is local to the device holding the referring object. This distinction matters for Schedule, Trend Log, Event Enrollment, Structured View, and many other objects that can reference data in remote devices. ### BACnet Specification References - **ASN.1** (Clause 21): `BACnetDeviceObjectReference ::= SEQUENCE { deviceIdentifier [0] BACnetObjectIdentifier OPTIONAL, objectIdentifier [1] BACnetObjectIdentifier }` - Used inside `BACnet.Protocol.ObjectPropertyRef`, `BACnet.Protocol.RecipientAddress`, many event and fault parameters, Structured View, etc. ### Examples (Doc Test) ```elixir iex> ref = %DeviceObjectRef{ ...> device_identifier: nil, ...> object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1} ...> } iex> ref.object_identifier.instance 1 ``` """ # TODO: Throw argument error in encode if not valid alias BACnet.Protocol.ApplicationTags alias BACnet.Protocol.ObjectIdentifier import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4] @typedoc """ References an object, optionally qualified by the device it resides in. """ @type t :: %__MODULE__{ device_identifier: ObjectIdentifier.t() | nil, object_identifier: ObjectIdentifier.t() } @fields [ :device_identifier, :object_identifier ] @enforce_keys @fields defstruct @fields @doc """ Encodes a BACnet device object reference into application tags encoding. """ @spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()} def encode(%__MODULE__{} = ref, opts \\ []) do with {:ok, devident, _header} <- (if ref.device_identifier do ApplicationTags.encode_value({:object_identifier, ref.device_identifier}, opts) else {:ok, nil, nil} end), {:ok, objident, _header} <- ApplicationTags.encode_value({:object_identifier, ref.object_identifier}, opts) do base = [ tagged: {0, devident, byte_size(devident || <<>>)}, tagged: {1, objident, byte_size(objident)} ] {:ok, Enum.filter(base, fn {_type, {_t, con, _l}} -> con end)} else {:error, _err} = err -> err end end @doc """ Parses a BACnet device object 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, device_identifier, ref} <- pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :object_identifier, true), {:ok, object_identifier, rest} <- pattern_extract_tags(ref, {:tagged, {1, _t, _l}}, :object_identifier, false) do objref = %__MODULE__{ device_identifier: device_identifier, object_identifier: object_identifier } {:ok, {objref, rest}} else {:error, :missing_pattern} -> {:error, :invalid_tags} {:error, _err} = err -> err end end @doc """ Validates whether the given device object 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, device_identifier: dev_ref } = _t ) do ObjectIdentifier.valid?(obj_ref) 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