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

lib/bacnet/protocol/bacnet_date_time.ex

defmodule BACnet.Protocol.BACnetDateTime do
@moduledoc """
A BACnet DateTime is used to represent date with timepoints.
It wraps both `BACnetDate` and `BACnetTime`.
This module provides some helpers to convert `DateTime` and `NaiveDateTime`
into a `BACnetDateTime` and back.
"""
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.BACnetDate
alias BACnet.Protocol.BACnetTime
@typedoc """
Represents a BACnet DateTime. It wraps both BACnet Date and Time.
"""
@type t :: %__MODULE__{
date: BACnetDate.t(),
time: BACnetTime.t()
}
@fields [
:date,
:time
]
@enforce_keys @fields
defstruct @fields
@doc """
Compares two BACnet DateTime.
Returns `:gt` if first datetime is later than the second,
and `:lt` for vice versa.
If the two datetimes are equal, `:eq` is returned.
Note that this is achieved by converting to `DateTime` and then
comparing them.
"""
@spec compare(t(), t()) :: :gt | :eq | :lt
def compare(%__MODULE__{} = dt1, %__MODULE__{} = dt2) do
DateTime.compare(to_datetime!(dt1), to_datetime!(dt2))
end
@doc """
Encodes the given BACnet DateTime into an application tag.
For tagged encoding, you'll have to strip this down further
using manual efforts.
"""
@spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{} = dt, _opts \\ []) do
{:ok, [{:date, dt.date}, {:time, dt.time}]}
end
@doc """
Converts a `DateTime` to a BACnet DateTime.
"""
@spec from_datetime(DateTime.t()) :: t()
def from_datetime(%DateTime{} = dt) do
%__MODULE__{
date: BACnetDate.from_date(DateTime.to_date(dt)),
time: BACnetTime.from_time(DateTime.to_time(dt))
}
end
@doc """
Converts a `NaiveDateTime` to a BACnet DateTime.
"""
@spec from_naive_datetime(NaiveDateTime.t()) :: t()
def from_naive_datetime(%NaiveDateTime{} = dt) do
%__MODULE__{
date: BACnetDate.from_date(NaiveDateTime.to_date(dt)),
time: BACnetTime.from_time(NaiveDateTime.to_time(dt))
}
end
@doc """
Parses a BACnet DateTime from BACnet 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 [{:date, %BACnetDate{} = date}, {:time, %BACnetTime{} = time} | rest] <- tags do
dt = %__MODULE__{
date: date,
time: time
}
{:ok, {dt, rest}}
else
_else -> {:error, :invalid_tags}
end
end
@doc """
Checks whether the given BACnet DateTime is a specific date-time value
(every component is a numeric value).
"""
@spec specific?(t()) :: boolean()
def specific?(%__MODULE__{} = dt) do
BACnetDate.specific?(dt.date) and BACnetTime.specific?(dt.time)
end
@doc """
Converts the BACnet DateTime to a `DateTime`.
"""
@spec to_datetime(t(), Calendar.time_zone(), Calendar.time_zone_database()) ::
{:ok, DateTime.t()} | {:error, term()}
def to_datetime(
%__MODULE__{} = dt,
timezone \\ Application.get_env(:bacstack, :default_timezone, "Etc/UTC"),
time_zone_database \\ Calendar.get_time_zone_database()
) do
with {:ok, date} <- BACnetDate.to_date(dt.date),
{:ok, time} <- BACnetTime.to_time(dt.time),
do: DateTime.new(date, time, timezone, time_zone_database)
end
@doc """
Bang-version of `to_datetime/1`.
"""
@spec to_datetime!(t(), Calendar.time_zone(), Calendar.time_zone_database()) ::
DateTime.t() | no_return()
def to_datetime!(
%__MODULE__{} = dt,
timezone \\ Application.get_env(:bacstack, :default_timezone, "Etc/UTC"),
time_zone_database \\ Calendar.get_time_zone_database()
) do
date = BACnetDate.to_date!(dt.date)
time = BACnetTime.to_time!(dt.time)
DateTime.new!(date, time, timezone, time_zone_database)
end
@doc """
Converts the BACnet DateTime to a `NaiveDateTime`.
"""
@spec to_naive_datetime(t()) :: {:ok, NaiveDateTime.t()} | {:error, term()}
def to_naive_datetime(%__MODULE__{} = dt) do
with {:ok, date} <- BACnetDate.to_date(dt.date),
{:ok, time} <- BACnetTime.to_time(dt.time),
do: NaiveDateTime.new(date, time)
end
@doc """
Bang-version of `to_naive_datetime/1`.
"""
@spec to_naive_datetime!(t()) :: NaiveDateTime.t() | no_return()
def to_naive_datetime!(%__MODULE__{} = dt) do
date = BACnetDate.to_date!(dt.date)
time = BACnetTime.to_time!(dt.time)
NaiveDateTime.new!(date, time)
end
@doc """
Creates a new BACnet DateTime for the current UTC datetime.
"""
@spec utc_now() :: t()
def utc_now() do
%__MODULE__{
date: BACnetDate.from_date(Date.utc_today()),
time: BACnetTime.from_time(Time.utc_now())
}
end
@doc """
Validates whether the given BACnet datetime is in form valid.
It only validates the struct is valid as per type specification.
"""
@spec valid?(t()) :: boolean()
def valid?(
%__MODULE__{
date: %BACnetDate{} = date,
time: %BACnetTime{} = time
} = _t
) do
BACnetDate.valid?(date) and BACnetTime.valid?(time)
end
def valid?(%__MODULE__{} = _t), do: false
end