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

lib/bacnet/protocol/cov_subscription.ex

defmodule BACnet.Protocol.CovSubscription do
  @moduledoc """
  A COV Subscription records that a particular client (identified by its
  Process Identifier and Recipient address) has asked to be notified whenever
  a specific property (or the entire object) changes value beyond a configured
  increment.

  The structure stores the subscription parameters that the server must remember:
  the recipient address (which may be a local process or a remote device), the
  monitored property reference, the COV increment, the issue confirmed flag,
  and the lifetime of the subscription.

  When the monitored value changes sufficiently, the server walks its subscription
  table and sends ConfirmedCOVNotification or UnconfirmedCOVNotification messages
  to every active subscriber. Proper management of these records (including
  automatic removal when the lifetime expires) is one of the more complex pieces
  of state that a BACnet server must maintain.

  ### Examples (Doc Test)

  ```elixir
  iex> sub = %CovSubscription{
  ...>   recipient: %Recipient{type: :device, device: %ObjectIdentifier{type: :device, instance: 100}, address: nil},
  ...>   recipient_process: 1,
  ...>   monitored_object_property: %ObjectPropertyRef{
  ...>     object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1},
  ...>     property_identifier: :present_value,
  ...>     property_array_index: nil
  ...>   },
  ...>   issue_confirmed_notifications: true,
  ...>   time_remaining: 3600,
  ...>   cov_increment: 0.5
  ...> }
  iex> sub.time_remaining
  3600
  ```
  """

  # TODO: Throw argument error in encode if not valid

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

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

  @typedoc """
  Represents a Change of Value (COV) subscription.
  """
  @type t :: %__MODULE__{
          recipient: Recipient.t(),
          recipient_process: non_neg_integer(),
          monitored_object_property: ObjectPropertyRef.t(),
          issue_confirmed_notifications: boolean(),
          time_remaining: non_neg_integer(),
          cov_increment: float() | nil
        }

  @fields [
    :recipient,
    :recipient_process,
    :monitored_object_property,
    :issue_confirmed_notifications,
    :time_remaining,
    :cov_increment
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a COV subscription struct into BACnet application tags encoding.
  """
  @spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{} = cov, opts \\ []) do
    with {:ok, [recipient]} <- Recipient.encode(cov.recipient, opts),
         {:ok, process_id, _header} <-
           ApplicationTags.encode_value({:unsigned_integer, cov.recipient_process}, opts),
         {:ok, obj_prop} <- ObjectPropertyRef.encode(cov.monitored_object_property, opts),
         {:ok, issue_confirmed} <-
           ApplicationTags.create_tag_encoding(2, {:boolean, cov.issue_confirmed_notifications}),
         {:ok, time_remaining} <-
           ApplicationTags.create_tag_encoding(3, {:unsigned_integer, cov.time_remaining}),
         {:ok, cov_increment} <-
           (if cov.cov_increment do
              ApplicationTags.create_tag_encoding(4, {:real, cov.cov_increment})
            else
              {:ok, nil}
            end) do
      tags = [
        {:constructed,
         {0,
          [
            constructed: {0, recipient, 0},
            tagged: {1, process_id, byte_size(process_id)}
          ], 0}},
        {:constructed, {1, obj_prop, 0}},
        issue_confirmed,
        time_remaining,
        cov_increment
      ]

      {:ok, Enum.reject(tags, &is_nil/1)}
    else
      {:error, _err} = err -> err
    end
  end

  @doc """
  Parses a BACnet COV subscription application tags encoding 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,
          {:constructed,
           {0,
            [
              constructed: {0, recipient_raw, _len},
              tagged: {1, process_identifier_raw, _len2}
            ], _len3}},
          rest} <-
           pattern_extract_tags(tags, {:constructed, {0, _c, _l}}, nil, false),
         {:ok, {recipient, _rest}} <- Recipient.parse([recipient_raw]),
         {:ok, {:unsigned_integer, process_id}} <-
           ApplicationTags.unfold_to_type(:unsigned_integer, process_identifier_raw),
         {:ok, {:constructed, {1, objpropref_raw, _len}}, rest} <-
           pattern_extract_tags(rest, {:constructed, {1, _c, _l}}, nil, false),
         {:ok, {objpropref, _rest}} <- ObjectPropertyRef.parse(objpropref_raw),
         {:ok, issue_confirmed, rest} <-
           pattern_extract_tags(rest, {:tagged, {2, _c, _l}}, :boolean, false),
         {:ok, time_remaining, rest} <-
           pattern_extract_tags(rest, {:tagged, {3, _c, _l}}, :unsigned_integer, false),
         {:ok, cov_increment, rest} <-
           pattern_extract_tags(rest, {:tagged, {4, _c, _l}}, :real, true) do
      cov = %__MODULE__{
        recipient: recipient,
        recipient_process: process_id,
        monitored_object_property: objpropref,
        issue_confirmed_notifications: issue_confirmed,
        time_remaining: time_remaining,
        cov_increment: cov_increment
      }

      {:ok, {cov, rest}}
    else
      {:ok, {:constructed, _term}, _rest} -> {:error, :invalid_tags}
      {:error, :missing_pattern} -> {:error, :invalid_tags}
      {:error, _err} = err -> err
    end
  end

  @doc """
  Validates whether the given COV subscription is in form valid.

  It only validates the struct is valid as per type specification.
  """
  @spec valid?(t()) :: boolean()
  def valid?(
        %__MODULE__{
          recipient: %Recipient{} = recipient,
          recipient_process: process_id,
          monitored_object_property: %ObjectPropertyRef{} = ref,
          issue_confirmed_notifications: issue_confirmed,
          time_remaining: time_remaining,
          cov_increment: cov_increment
        } = _t
      )
      when is_integer(process_id) and process_id >= 0 and process_id <= 4_294_967_295 and
             is_boolean(issue_confirmed) and
             is_integer(time_remaining) and time_remaining >= 0 and
             (is_nil(cov_increment) or is_float(cov_increment)) do
    Recipient.valid?(recipient) and ObjectPropertyRef.valid?(ref)
  end

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