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