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

lib/bacnet/protocol/bacnet_timestamp.ex

defmodule BACnet.Protocol.BACnetTimestamp do
# TODO: Docs
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.BACnetDateTime
alias BACnet.Protocol.BACnetTime
@type t :: %__MODULE__{
type: :time | :sequence_number | :datetime,
time: BACnetTime.t() | nil,
sequence_number: non_neg_integer() | nil,
datetime: BACnetDateTime.t() | nil
}
@fields [
:type,
:time,
:sequence_number,
:datetime
]
@enforce_keys @fields
defstruct @fields
@doc """
Encodes the given BACnet timestamp into an application tag.
"""
@spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(timestamp, opts \\ [])
def encode(%__MODULE__{type: :time} = timestamp, opts) do
with {:ok, value, _header} <- ApplicationTags.encode_value({:time, timestamp.time}, opts) do
{:ok, [{:tagged, {0, value, byte_size(value)}}]}
end
end
def encode(%__MODULE__{type: :sequence_number} = timestamp, opts) do
with {:ok, value, _header} <-
ApplicationTags.encode_value({:unsigned_integer, timestamp.sequence_number}, opts) do
{:ok, [{:tagged, {1, value, byte_size(value)}}]}
end
end
def encode(%__MODULE__{type: :datetime, datetime: %BACnetDateTime{} = dt} = _timestamp, _opts) do
{:ok, [{:constructed, {2, [date: dt.date, time: dt.time], 0}}]}
end
@doc """
Decodes the given application tags encoding into a BACnet timestamp.
Example:
iex> BACnetTimestamp.parse([{:tagged, {0, <<2, 12, 49, 0>>, 4}}])
{:ok,
{%BACnetTimestamp{
datetime: nil,
sequence_number: nil,
time: %BACnetTime{
hour: 2,
hundredth: 0,
minute: 12,
second: 49
},
type: :time
}, []}}
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term}
def parse(tags) when is_list(tags) do
with [head | rest] <- tags,
{:ok, result} <- do_parse(head) do
{:ok, {result, rest}}
else
[] -> {:error, :invalid_tags}
{:error, :invalid_data} -> {:error, :invalid_tags}
{:error, :unknown_tag_encoding} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@spec do_parse(ApplicationTags.encoding()) :: {:ok, t()} | {:error, term}
defp do_parse({:tagged, {0, _value, 4}} = value) do
case ApplicationTags.unfold_to_type(:time, value) do
{:ok, {:time, time}} ->
result = %__MODULE__{
type: :time,
time: time,
sequence_number: nil,
datetime: nil
}
{:ok, result}
{:error, _term} = term ->
term
end
end
defp do_parse({:tagged, {1, _value, _len}} = value) do
case ApplicationTags.unfold_to_type(:unsigned_integer, value) do
{:ok, {:unsigned_integer, seqnum}} ->
result = %__MODULE__{
type: :sequence_number,
sequence_number: seqnum,
datetime: nil,
time: nil
}
{:ok, result}
{:error, _term} = term ->
term
end
end
defp do_parse({:constructed, {2, ts, _len}}) when is_list(ts) do
with {:ok, {dt, _rest}} <- BACnetDateTime.parse(ts) do
result = %__MODULE__{
type: :datetime,
datetime: dt,
sequence_number: nil,
time: nil
}
{:ok, result}
else
_term -> {:error, :invalid_tags}
end
end
defp do_parse(_arg), do: {:error, :invalid_tags}
@doc """
Validates whether the given BACnet timestamp 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__{
type: :time,
time: %BACnetTime{} = time
} = _t
) do
BACnetTime.valid?(time)
end
def valid?(
%__MODULE__{
type: :sequence_number,
sequence_number: num
} = _t
)
when is_integer(num) and num >= 0,
do: true
def valid?(
%__MODULE__{
type: :datetime,
datetime: %BACnetDateTime{} = dt
} = _t
) do
BACnetDateTime.valid?(dt)
end
def valid?(%__MODULE__{} = _t), do: false
end