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

lib/bacnet/protocol/device_object_ref.ex

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