Current section
Files
Jump to
Current section
Files
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