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

lib/bacnet/protocol/setpoint_reference.ex

defmodule BACnet.Protocol.SetpointReference do
  @moduledoc """
  A Setpoint Reference is a nullable pointer to another property that should be
  treated as the setpoint for a control loop object (Loop, Analog Output, etc.).

  When the reference is present, the control algorithm is expected to treat the
  value of the referenced property as its current setpoint rather than using a
  locally configured setpoint. This indirection is extremely useful in cascaded
  control strategies, in demand-limiting applications, and anywhere the desired
  setpoint is computed or selected by some other object in the system.

  The reference uses the common Object Property Reference type so that it can
  point at properties in the same device or, when the full Device + Object form
  is used, at properties in other devices.

  ### Examples (Doc Test)

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

  # TODO: Throw argument error in encode if not valid

  alias BACnet.Protocol.ApplicationTags
  alias BACnet.Protocol.ObjectPropertyRef

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

  @typedoc """
  Optionally references another property to be used as the setpoint for a control loop.
  """
  @type t :: %__MODULE__{
          ref: ObjectPropertyRef.t() | nil
        }

  @fields [
    :ref
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a BACnet setpoint reference into application tags encoding.
  """
  @spec encode(t(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{} = setpoint, opts \\ []) do
    if setpoint.ref do
      with {:ok, ref} <- ObjectPropertyRef.encode(setpoint.ref, opts) do
        {:ok, [constructed: {0, ref, 0}]}
      end
    else
      {:ok, []}
    end
  end

  @doc """
  Parses a BACnet setpoint reference from application tags encoding.
  """
  @spec parse(ApplicationTags.encoding_list()) ::
          {:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term()}
  def parse(tags) when is_list(tags) do
    with {:ok, {:constructed, {0, ref_raw, _len}}, rest} when ref_raw != [] <-
           pattern_extract_tags(tags, {:constructed, {0, _t, _l}}, nil, true),
         {:ok, {ref, _rest}} <- ObjectPropertyRef.parse(List.wrap(ref_raw)) do
      setp = %__MODULE__{
        ref: ref
      }

      {:ok, {setp, rest}}
    else
      {:ok, {:constructed, {0, _raw, _len}}, rest} -> {:ok, {%__MODULE__{ref: nil}, rest}}
      {:ok, nil, rest} -> {:ok, {%__MODULE__{ref: nil}, rest}}
      {:error, _err} = err -> err
    end
  end

  @doc """
  Validates whether the given setpoint reference is in form valid.

  It only validates the struct is valid as per type specification.
  """
  @spec valid?(t()) :: boolean()
  def valid?(t)

  def valid?(
        %__MODULE__{
          ref: %ObjectPropertyRef{} = ref
        } = _t
      ) do
    ObjectPropertyRef.valid?(ref)
  end

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