Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/calendar_entry.ex
defmodule BACnet.Protocol.CalendarEntry do
@moduledoc """
A Calendar Entry is the CHOICE type used by both Calendar objects and Schedule
objects to describe "special" days (holidays, maintenance periods, after-hours
operation, etc.).
The three alternatives (defined in Clause 21) are:
- `:date` - a single specific (or partially unspecified) `BACnet.Protocol.BACnetDate`
- `:date_range` - an inclusive `BACnet.Protocol.DateRange`
- `:week_n_day` - a recurring `BACnet.Protocol.WeekNDay` pattern
This single type is deliberately reused in multiple places so that matching
logic ("does today match any exception?") can be written once.
### BACnet Specification References
- **ASN.1** (Clause 21):
```
BACnetCalendarEntry ::= CHOICE {
date Date,
dateRange BACnetDateRange,
weekNDay BACnetWeekNDay
}
```
- **Calendar object** (12.9): `date_list` property is `BACnetARRAY` of `BACnet.Protocol.CalendarEntry`.
- **Schedule object** (12.24): `exception_schedule` contains `BACnet.Protocol.SpecialEvent`
whose `period` can be a `BACnet.Protocol.CalendarEntry` or an object identifier referencing
a Calendar object.
### Examples
#### Specific date entry
```elixir
iex> entry = %CalendarEntry{type: :date, date: %BACnetDate{year: 2025, month: 12, day: 25, weekday: 4}, date_range: nil, week_n_day: nil}
iex> entry.type
:date
```
#### Date range entry
```elixir
iex> entry = %CalendarEntry{type: :date_range, date_range: %DateRange{start_date: %BACnetDate{year: 2025, month: 7, day: 1, weekday: 2}, end_date: %BACnetDate{year: 2025, month: 7, day: 31, weekday: 4}}, date: nil, week_n_day: nil}
iex> entry.type
:date_range
```
#### Recurring WeekNDay entry
```elixir
iex> entry = %CalendarEntry{type: :week_n_day, week_n_day: %WeekNDay{month: :unspecified, week_of_month: 3, weekday: 2}, date: nil, date_range: nil}
iex> entry.type
:week_n_day
```
### See Also
- `BACnet.Protocol.DailySchedule`
- `BACnet.Protocol.DateRange`
- `BACnet.Protocol.DaysOfWeek`
- `BACnet.Protocol.SpecialEvent`
- `BACnet.Protocol.WeekNDay`
"""
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.BACnetDate
alias BACnet.Protocol.DateRange
alias BACnet.Protocol.WeekNDay
@typedoc """
CHOICE of a single date, date range, or `BACnet.Protocol.WeekNDay` pattern
(see `BACnet.Protocol.CalendarEntry` module docs + ASN.1 in Clause 21).
Used in Calendar `date_list` and Schedule exception schedules.
"""
@type t :: %__MODULE__{
type: :date | :date_range | :week_n_day,
date: BACnetDate.t() | nil,
date_range: DateRange.t() | nil,
week_n_day: WeekNDay.t() | nil
}
@fields [:type, :date, :date_range, :week_n_day]
@enforce_keys @fields
defstruct @fields
@doc """
Encode a calendar entry into application tag-encoded.
"""
@spec encode(t(), Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(entry, opts \\ [])
def encode(%__MODULE__{type: :date, date: %BACnetDate{} = date} = _entry, opts) do
with {:ok, date, _header} <- ApplicationTags.encode_value({:date, date}, opts) do
{:ok, [{:tagged, {0, date, byte_size(date)}}]}
end
end
def encode(
%__MODULE__{
type: :date_range,
date_range: %DateRange{
start_date: %BACnetDate{} = start_date,
end_date: %BACnetDate{} = end_date
}
} = _entry,
_opts
) do
{:ok,
[
{:constructed,
{1,
[
date: start_date,
date: end_date
], 0}}
]}
end
def encode(%__MODULE__{type: :week_n_day, week_n_day: %WeekNDay{} = week_n_day} = _entry, _opts) do
with {:ok, [{:octet_string, str}]} <- WeekNDay.encode(week_n_day) do
{:ok, [{:tagged, {2, str, 3}}]}
end
end
@doc """
Parse application tag-encoded calendar entry into a struct.
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}}
| {:error, term()}
def parse(tags) when is_list(tags) do
case tags do
[{:tagged, {0, tags, _len}} | rest] ->
with {:ok, {:date, date}} <- ApplicationTags.unfold_to_type(:date, tags) do
cal = %__MODULE__{
type: :date,
date: date,
date_range: nil,
week_n_day: nil
}
{:ok, {cal, rest}}
end
[
{:constructed,
{1,
[
{:date, start_date},
{:date, end_date}
], _len}}
| rest
] ->
cal = %__MODULE__{
type: :date_range,
date: nil,
date_range: %DateRange{
start_date: start_date,
end_date: end_date
},
week_n_day: nil
}
{:ok, {cal, rest}}
[{:tagged, {2, str, _len}} | rest] ->
with {:ok, {week, _rest}} <- WeekNDay.parse([{:octet_string, str}]) do
cal = %__MODULE__{
type: :week_n_day,
date: nil,
date_range: nil,
week_n_day: week
}
{:ok, {cal, rest}}
end
_term ->
{:error, :invalid_tags}
end
end
@doc """
Validates whether the given BACnet calendar entry 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: :date,
date: %BACnetDate{} = date
} = _t
) do
BACnetDate.valid?(date)
end
def valid?(
%__MODULE__{
type: :date_range,
date_range: %DateRange{} = range
} = _t
) do
DateRange.valid?(range)
end
def valid?(
%__MODULE__{
type: :week_n_day,
week_n_day: %WeekNDay{} = week
} = _t
) do
WeekNDay.valid?(week)
end
def valid?(%__MODULE__{} = _t), do: false
end