Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/special_event.ex
defmodule BACnet.Protocol.SpecialEvent do
# TODO: Docs
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.CalendarEntry
alias BACnet.Protocol.Constants
alias BACnet.Protocol.ObjectIdentifier
alias BACnet.Protocol.TimeValue
import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4]
require Constants
@typedoc """
Represents a BACnet Special Event.
"""
@type t :: %__MODULE__{
period: CalendarEntry.t() | ObjectIdentifier.t(),
list: [TimeValue.t()],
priority: 1..16
}
@fields [
:period,
:list,
:priority
]
@enforce_keys @fields
defstruct @fields
@doc """
Encode a BACnet special event into application tag-encoded.
"""
@spec encode(t(), Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{} = event, opts \\ []) do
with {:ok, period} <-
(case event.period do
%CalendarEntry{} ->
with {:ok, [entry]} <- CalendarEntry.encode(event.period) do
{:ok, {:constructed, {0, entry, 0}}}
end
%ObjectIdentifier{} ->
ApplicationTags.create_tag_encoding(1, :object_identifier, event.period)
_else ->
{:error, :invalid_period}
end),
{:ok, schedules} <-
Enum.reduce_while(event.list, {:ok, []}, fn
%TimeValue{} = tv, {:ok, acc} ->
case TimeValue.encode(tv, opts) do
{:ok, encoded} -> {:cont, {:ok, [encoded | acc]}}
{:error, _err} = err -> {:halt, err}
end
_else, _acc ->
{:halt, {:error, :invalid_time_value}}
end),
{:ok, priority} <-
ApplicationTags.create_tag_encoding(3, :unsigned_integer, event.priority) do
event_list =
schedules
|> Enum.reverse()
|> List.flatten()
params = [
period,
{:constructed, {2, event_list, 0}},
priority
]
{:ok, params}
else
{:error, _err} = err -> err
end
end
@doc """
Parse a BACnet special event 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, period, rest} <- parse_period(tags),
{:ok, {:constructed, {2, schedule_raw, 0}}, rest} <-
pattern_extract_tags(rest, {:constructed, {2, _v, _l}}, nil, false),
{:ok, schedules} when is_list(schedules) <-
Enum.reduce_while(1..100_000//1, {:ok, {[], schedule_raw}}, fn
_ind, {:ok, {acc, []}} ->
{:halt, {:ok, acc}}
_ind, {:ok, {acc, tags}} ->
case TimeValue.parse(tags) do
{:ok, {tv, rest}} -> {:cont, {:ok, {[tv | acc], rest}}}
{:error, _err} = err -> {:halt, err}
end
end),
{:ok, priority, rest} <-
pattern_extract_tags(rest, {:tagged, {3, _c, _l}}, :unsigned_integer, false) do
schedule = %__MODULE__{
period: period,
list: Enum.reverse(schedules),
priority: priority
}
{:ok, {schedule, rest}}
else
{:error, :missing_pattern} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@doc """
Validates whether the given special event 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__{
period: %CalendarEntry{} = period,
list: list,
priority: priority
} = _t
)
when is_list(list) and priority in 1..16 do
CalendarEntry.valid?(period) and validate_list(list)
end
def valid?(
%__MODULE__{
period:
%ObjectIdentifier{
type: Constants.macro_assert_name(:object_type, :calendar)
} = period,
list: list,
priority: priority
} = _t
)
when is_list(list) and priority in 1..16 do
ObjectIdentifier.valid?(period) and validate_list(list)
end
def valid?(%__MODULE__{} = _t), do: false
@spec parse_period(ApplicationTags.encoding_list()) ::
{:ok, CalendarEntry.t() | ObjectIdentifier.t(), ApplicationTags.encoding_list()}
| {:error, term()}
defp parse_period(tags)
defp parse_period([{:constructed, {0, raw, 0}} | rest]) do
with {:ok, {cal, _rest}} <- CalendarEntry.parse(List.wrap(raw)) do
{:ok, cal, rest}
end
end
defp parse_period([{:tagged, {1, _raw, _len}} = tag | rest]) do
with {:ok, {:object_identifier, obj}} <-
ApplicationTags.unfold_to_type(:object_identifier, tag) do
{:ok, obj, rest}
end
end
defp parse_period(_else), do: {:error, :invalid_tags}
defp validate_list(list) when is_list(list) do
Enum.all?(list, fn
%TimeValue{} = val -> TimeValue.valid?(val)
_else -> false
end)
end
end