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

lib/bacnet/protocol/enrollment_summary.ex

defmodule BACnet.Protocol.EnrollmentSummary do
  @moduledoc """
  An Enrollment Summary entry is produced by the Get Enrollment Summary service.
  It lists the objects inside a device that have been configured to generate event
  notifications (intrinsic or algorithmic) to a particular notification class or
  recipient, filtered by the parameters supplied in the request.

  Each entry carries the object identifier, the event type that the object is
  configured to generate, the current event state, the priority that will be used
  in notifications, and optionally the notification class. This information is
  primarily useful for management stations that need to discover which objects
  will send alarms to a given operator or system, or to audit the event
  subscription configuration of a large site.

  The service supports several different "acknowledgment filter" modes so that a
  client can ask for only unacknowledged alarms, only acknowledged alarms, or all
  enrollments regardless of acknowledgment state.
  """

  # TODO: Throw argument error in encode if not valid

  alias BACnet.Protocol.ApplicationTags
  alias BACnet.Protocol.Constants
  alias BACnet.Protocol.ObjectIdentifier

  @typedoc """
  Represents a single entry returned by the Get Enrollment Summary service.
  """
  @type t :: %__MODULE__{
          object_identifier: ObjectIdentifier.t(),
          event_type: Constants.event_type(),
          event_state: Constants.event_state(),
          priority: byte(),
          notification_class: non_neg_integer() | nil
        }

  @fields [
    :object_identifier,
    :event_type,
    :event_state,
    :priority,
    :notification_class
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a BACnet enrollment summary into BACnet application tags encoding.
  """
  @spec encode(t(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(
        %__MODULE__{} = summary,
        _opts \\ []
      ) do
    with :ok <-
           if(ApplicationTags.valid_int?(summary.priority, 8),
             do: :ok,
             else: {:error, :invalid_priority_value}
           ),
         {:ok, event_type} <-
           Constants.by_name_with_reason(
             :event_type,
             summary.event_type,
             {:unknown_type, summary.event_type}
           ),
         {:ok, event_state} <-
           Constants.by_name_with_reason(
             :event_state,
             summary.event_state,
             {:unknown_state, summary.event_state}
           ) do
      tail =
        if summary.notification_class do
          [{:unsigned_integer, summary.notification_class}]
        else
          []
        end

      params = [
        {:object_identifier, summary.object_identifier},
        {:enumerated, event_type},
        {:enumerated, event_state},
        {:unsigned_integer, summary.priority}
        | tail
      ]

      {:ok, params}
    end
  end

  @doc """
  Parses a BACnet enrollment summary from BACnet application tags encoding.
  """
  @spec parse(ApplicationTags.encoding_list()) ::
          {:ok, {t(), rest :: ApplicationTags.encoding_list()}}
          | {:error, term()}
  def parse(tags) when is_list(tags) do
    case tags do
      [
        {:object_identifier, obj},
        {:enumerated, event_type},
        {:enumerated, event_state},
        {:unsigned_integer, priority}
        # Parse optional notification class in function body
        | rest
      ] ->
        with :ok <-
               if(ApplicationTags.valid_int?(priority, 8),
                 do: :ok,
                 else: {:error, :invalid_priority_value}
               ),
             {:ok, event_type_c} <-
               Constants.by_value_with_reason(
                 :event_type,
                 event_type,
                 {:unknown_type, event_type}
               ),
             {:ok, event_state_c} <-
               Constants.by_value_with_reason(
                 :event_state,
                 event_state,
                 {:unknown_state, event_state}
               ) do
          {notif_class, rest} =
            case rest do
              [{:unsigned_integer, notif_class} | rest] -> {notif_class, rest}
              _term -> {nil, rest}
            end

          summary = %__MODULE__{
            object_identifier: obj,
            event_type: event_type_c,
            event_state: event_state_c,
            priority: priority,
            notification_class: notif_class
          }

          {:ok, {summary, rest}}
        end

      _else ->
        {:error, :invalid_tags}
    end
  end

  @doc """
  Validates whether the given enrollment summary 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,
          event_type: type,
          event_state: state,
          priority: priority,
          notification_class: class
        } = _t
      )
      when is_integer(priority) and priority >= 0 and priority <= 255 and
             (is_nil(class) or (is_integer(class) and class >= 0 and class <= 4_294_967_295)) do
    ObjectIdentifier.valid?(obj_ref) and Constants.has_by_name(:event_type, type) and
      Constants.has_by_name(:event_state, state)
  end

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