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

lib/bacnet/protocol/event_log_record.ex

defmodule BACnet.Protocol.EventLogRecord do
  @moduledoc """
  An Event Log Record is the structure stored by an Event Log object every time
  it receives (or generates) an event notification. It captures the timestamp,
  the full Confirmed Event Notification that was processed, the log status or
  any time change, at the moment the record was created.

  Event logs are the BACnet equivalent of an alarm and event journal. They
  provide a time-stamped history of every significant event that occurred
  in the system, which is invaluable for post-incident analysis,
  regulatory compliance, and troubleshooting. Because the original notification
  is stored verbatim (including all parameters and source information), a
  later review can see exactly what the alarming object reported.
  """

  # TODO: Throw argument error in encode if not valid

  alias BACnet.Protocol.APDU.ConfirmedServiceRequest
  alias BACnet.Protocol.ApplicationTags
  alias BACnet.Protocol.BACnetDateTime
  alias BACnet.Protocol.Constants
  alias BACnet.Protocol.LogStatus
  alias BACnet.Protocol.Services.Common
  alias BACnet.Protocol.Services.ConfirmedEventNotification

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

  @typedoc """
  Represents a single record stored in an Event Log object.
  """
  @type t :: %__MODULE__{
          timestamp: BACnetDateTime.t(),
          log_datum:
            LogStatus.t()
            | ConfirmedEventNotification.t()
            | {:time_change, float()}
        }

  @fields [
    :timestamp,
    :log_datum
  ]
  @enforce_keys @fields
  defstruct @fields

  @doc """
  Encode a BACnet log record into application tag-encoded.
  """
  @spec encode(t(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding_list()} | {:error, term()}
  def encode(%__MODULE__{} = record, opts \\ []) do
    case encode_log_datum(record.log_datum, opts) do
      {:ok, log_datum} ->
        base = [
          {:constructed, {0, [date: record.timestamp.date, time: record.timestamp.time], 0}},
          {:constructed, {1, log_datum, 0}}
        ]

        {:ok, base}

      {:error, _err} = err ->
        err
    end
  end

  @doc """
  Parse a BACnet event log record 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,
            [
              date: date,
              time: time
            ], _l}},
          rest} <-
           pattern_extract_tags(tags, {:constructed, {0, _t, _l}}, nil, false),
         {:ok, {:constructed, {1, log_datum_raw, _l}}, rest} <-
           pattern_extract_tags(rest, {:constructed, {1, _t, _l}}, nil, false),
         {:ok, log_datum} <- parse_log_datum(log_datum_raw) do
      record = %__MODULE__{
        timestamp: %BACnetDateTime{
          date: date,
          time: time
        },
        log_datum: log_datum
      }

      {:ok, {record, 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 event log record 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__{
          timestamp: %BACnetDateTime{} = stamp,
          log_datum: %LogStatus{} = datum
        } = _t
      ) do
    BACnetDateTime.valid?(stamp) and LogStatus.valid?(datum)
  end

  def valid?(
        %__MODULE__{
          timestamp: %BACnetDateTime{} = stamp,
          log_datum: %ConfirmedEventNotification{}
        } = _t
      ) do
    BACnetDateTime.valid?(stamp)
  end

  def valid?(
        %__MODULE__{
          timestamp: %BACnetDateTime{} = stamp,
          log_datum: {:time_change, value}
        } = _t
      )
      when is_float(value) do
    BACnetDateTime.valid?(stamp)
  end

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

  @spec parse_log_datum(ApplicationTags.encoding()) ::
          {:ok, term()} | {:error, term()}
  defp parse_log_datum(tags)

  defp parse_log_datum({:tagged, {0, _tags, _len}} = tags) do
    case ApplicationTags.unfold_to_type(:bitstring, tags) do
      {:ok, {:bitstring, {_b0, _b1, _b2} = bits}} ->
        {:ok, LogStatus.from_bitstring(bits)}

      {:ok, _val} ->
        {:error, :invalid_tags}

      {:error, _err} = err ->
        err
    end
  end

  defp parse_log_datum({:constructed, {1, tags, 0}}) do
    # Make up some data (they are not relevant)
    service = %ConfirmedServiceRequest{
      segmented_response_accepted: false,
      max_apdu: 1467,
      max_segments: :unspecified,
      invoke_id: 0,
      sequence_number: nil,
      proposed_window_size: nil,
      service:
        Constants.macro_assert_name(:confirmed_service_choice, :confirmed_event_notification),
      parameters: tags
    }

    ConfirmedEventNotification.from_apdu(service)
  end

  defp parse_log_datum({:tagged, {2, _tags, 4}} = tags) do
    case ApplicationTags.unfold_to_type(:real, tags) do
      {:ok, {:real, real}} ->
        {:ok, {:time_change, real}}

      {:error, _err} = err ->
        err
    end
  end

  defp parse_log_datum(_tags), do: {:error, :invalid_tags}

  @spec encode_log_datum(term(), Keyword.t()) ::
          {:ok, ApplicationTags.encoding()} | {:error, term()}
  defp encode_log_datum(value, opts)

  defp encode_log_datum(%LogStatus{} = value, _opts) do
    ApplicationTags.create_tag_encoding(0, LogStatus.to_bitstring(value))
  end

  defp encode_log_datum(%ConfirmedEventNotification{} = value, _opts) do
    case Common.encode_event_notification(value) do
      {:ok, %{parameters: params}} ->
        {:ok, {:constructed, {1, params, 0}}}

      {:error, _err} = err ->
        err
    end
  end

  defp encode_log_datum({:time_change, value}, _opts) when is_float(value) do
    ApplicationTags.create_tag_encoding(2, :real, value)
  end
end