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

lib/bacnet/protocol/event_information.ex

defmodule BACnet.Protocol.EventInformation do
  @moduledoc """
  Event Information is the rich data structure returned by the Get Event Information
  service (ASHRAE 135 Clause 13.12). It is the modern replacement for the older
  Get Alarm Summary service and is the recommended way for a client to obtain a
  complete picture of every object in a device that is capable of generating events.

  Each entry contains the object identifier, its current event state, the acknowledged
  transition bits, the timestamps of the three state transitions, the notify type,
  the event enable bits, and the three priority values that will be used for
  notifications at each transition. This level of detail allows a sophisticated
  operator workstation to present accurate alarm lists, compute alarm durations,
  and correctly route or escalate notifications.
  """

  # TODO: Throw argument error in encode if not valid

  alias BACnet.Protocol.ApplicationTags
  alias BACnet.Protocol.Constants
  alias BACnet.Protocol.EventTimestamps
  alias BACnet.Protocol.EventTransitionBits
  alias BACnet.Protocol.ObjectIdentifier

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

  @typedoc """
  Represents a single entry in the response of a Get Event Information service.
  """
  @type t :: %__MODULE__{
          object_identifier: ObjectIdentifier.t(),
          event_state: Constants.event_state(),
          acknowledged_transitions: EventTransitionBits.t(),
          event_timestamps: EventTimestamps.t(),
          notify_type: Constants.notify_type(),
          event_enable: EventTransitionBits.t(),
          event_priorities: {to_offnormal :: byte(), to_fault :: byte(), to_normal :: byte()}
        }

  @fields [
    :object_identifier,
    :event_state,
    :acknowledged_transitions,
    :event_timestamps,
    :notify_type,
    :event_enable,
    :event_priorities
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encodes a BACnet event information into BACnet application tags encoding.
  """
  @spec encode(t(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{event_priorities: {ev_prio1, ev_prio2, ev_prio3}} = event, _opts \\ []) do
    with {:ok, obj, _header} <-
           ApplicationTags.encode_value({:object_identifier, event.object_identifier}),
         {:ok, event_state_c} <-
           Constants.by_name_with_reason(
             :event_state,
             event.event_state,
             {:unknown_state, event.event_state}
           ),
         {:ok, event_state, _header} <-
           ApplicationTags.encode_value({:enumerated, event_state_c}),
         {:ok, event_transbits, _header} <-
           ApplicationTags.encode_value(
             EventTransitionBits.to_bitstring(event.acknowledged_transitions)
           ),
         {:ok, timestamps} <- EventTimestamps.encode(event.event_timestamps),
         {:ok, notify_type_c} <-
           Constants.by_name_with_reason(
             :notify_type,
             event.notify_type,
             {:unknown_notify_type, event.notify_type}
           ),
         {:ok, notify_type, _header} <-
           ApplicationTags.encode_value({:enumerated, notify_type_c}),
         {:ok, event_enable, _header} <-
           ApplicationTags.encode_value(EventTransitionBits.to_bitstring(event.event_enable)) do
      params = [
        {:tagged, {0, obj, byte_size(obj)}},
        {:tagged, {1, event_state, byte_size(event_state)}},
        {:tagged, {2, event_transbits, byte_size(event_transbits)}},
        {:constructed, {3, timestamps, 0}},
        {:tagged, {4, notify_type, byte_size(notify_type)}},
        {:tagged, {5, event_enable, byte_size(event_enable)}},
        {:constructed,
         {6,
          [
            {:unsigned_integer, ev_prio1},
            {:unsigned_integer, ev_prio2},
            {:unsigned_integer, ev_prio3}
          ], 0}}
      ]

      {:ok, params}
    end
  end

  @doc """
  Parses a BACnet event information 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
    with {:ok, obj, rest} <-
           pattern_extract_tags(tags, {:tagged, {0, _c, _l}}, :object_identifier, false),
         {:ok, event_state, rest} <-
           pattern_extract_tags(rest, {:tagged, {1, _c, _l}}, :enumerated, false),
         {:ok, event_state_c} <-
           Constants.by_value_with_reason(
             :event_state,
             event_state,
             {:unknown_state, event_state}
           ),
         {:ok, {offnormal, fault, normal}, rest} <-
           pattern_extract_tags(rest, {:tagged, {2, _c, _l}}, :bitstring, false),
         {:ok, {:constructed, {3, event_timestamps_raw, _len}}, rest} <-
           pattern_extract_tags(rest, {:constructed, {3, _c, _l}}, nil, false),
         {:ok, {event_timestamps, _rest}} <- EventTimestamps.parse(event_timestamps_raw),
         {:ok, notify_type, rest} <-
           pattern_extract_tags(rest, {:tagged, {4, _c, _l}}, :enumerated, false),
         {:ok, notify_type_c} <-
           Constants.by_value_with_reason(
             :notify_type,
             notify_type,
             {:unknown_notify_type, notify_type}
           ),
         {:ok, {ev_offnormal, ev_fault, ev_normal}, rest} <-
           pattern_extract_tags(rest, {:tagged, {5, _c, _l}}, :bitstring, false),
         {:ok,
          {:constructed,
           {6,
            [
              unsigned_integer: prio1,
              unsigned_integer: prio2,
              unsigned_integer: prio3
            ], _len}},
          rest} <-
           pattern_extract_tags(rest, {:constructed, {6, _c, _l}}, nil, false) do
      info = %__MODULE__{
        object_identifier: obj,
        event_state: event_state_c,
        acknowledged_transitions: %EventTransitionBits{
          to_offnormal: offnormal,
          to_fault: fault,
          to_normal: normal
        },
        event_timestamps: event_timestamps,
        notify_type: notify_type_c,
        event_enable: %EventTransitionBits{
          to_offnormal: ev_offnormal,
          to_fault: ev_fault,
          to_normal: ev_normal
        },
        event_priorities: {prio1, prio2, prio3}
      }

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

  @doc """
  Validates whether the given event information 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_state: state,
          acknowledged_transitions: %EventTransitionBits{} = ack_trans,
          event_timestamps: %EventTimestamps{} = event,
          notify_type: type,
          event_enable: %EventTransitionBits{} = enable,
          event_priorities: {to_offnormal, to_fault, to_normal}
        } = _t
      )
      when is_integer(to_offnormal) and to_offnormal >= 0 and to_offnormal <= 255 and
             is_integer(to_fault) and to_fault >= 0 and to_fault <= 255 and
             is_integer(to_normal) and to_normal >= 0 and to_normal <= 255 do
    ObjectIdentifier.valid?(obj_ref) and Constants.has_by_name(:event_state, state) and
      EventTransitionBits.valid?(ack_trans) and EventTimestamps.valid?(event) and
      Constants.has_by_name(:notify_type, type) and EventTransitionBits.valid?(enable)
  end

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