defmodule BACnet.Protocol.Destination do @moduledoc """ A Destination describes where and when event notifications should be sent for a particular recipient. It is the core data structure used inside Notification Class objects to define the distribution list for alarms and events. Each destination combines a Recipient (which can be a device, a broadcast address, or an unicast address), a process identifier that the recipient will use to correlate the notification, a flag indicating whether confirmed or unconfirmed notifications should be used, the set of transitions (to-offnormal, to-fault, to-normal) that should trigger a notification, and a time window (valid days + from/to time) during which the subscription is active. This structure allows very fine-grained control over alarm routing. For example, a critical high-priority alarm can be sent 24/7 to an operator workstation with confirmed notifications, while the same alarm can be sent only during business hours via unconfirmed notifications to a logging server. The time and day restrictions are evaluated by the Notification Class object itself when deciding whether to forward an event. ### Examples (Doc Test) ```elixir iex> destination = %Destination{ ...> recipient: %Recipient{type: :device, device: %ObjectIdentifier{type: :device, instance: 123}, address: nil}, ...> process_identifier: 1, ...> issue_confirmed_notifications: true, ...> transitions: %EventTransitionBits{to_offnormal: true, to_fault: true, to_normal: true}, ...> valid_days: %DaysOfWeek{monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false}, ...> from_time: %BACnetTime{hour: 8, minute: 0, second: 0, hundredth: 0}, ...> to_time: %BACnetTime{hour: 18, minute: 0, second: 0, hundredth: 0} ...> } iex> destination.process_identifier 1 ``` """ # TODO: Throw argument error in encode if not valid alias BACnet.Protocol.ApplicationTags alias BACnet.Protocol.BACnetTime alias BACnet.Protocol.DaysOfWeek alias BACnet.Protocol.EventTransitionBits alias BACnet.Protocol.Recipient @typedoc """ Represents the target and timing constraints for an event or COV notification. """ @type t :: %__MODULE__{ recipient: Recipient.t(), process_identifier: ApplicationTags.unsigned32(), issue_confirmed_notifications: boolean(), transitions: EventTransitionBits.t(), valid_days: DaysOfWeek.t(), from_time: BACnetTime.t(), to_time: BACnetTime.t() } @fields [ :recipient, :process_identifier, :issue_confirmed_notifications, :transitions, :valid_days, :from_time, :to_time ] @enforce_keys @fields defstruct @fields @doc """ Encodes a BACnet destination into BACnet application tags encoding. """ @spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()} def encode(%__MODULE__{} = destination, _opts \\ []) do with :ok <- if( destination.process_identifier >= 0 and ApplicationTags.valid_int?(destination.process_identifier, 32), do: :ok, else: {:error, :invalid_process_identifier_value} ), {:ok, [recipient_raw]} <- Recipient.encode(destination.recipient) do params = [ DaysOfWeek.to_bitstring(destination.valid_days), {:time, destination.from_time}, {:time, destination.to_time}, recipient_raw, {:unsigned_integer, destination.process_identifier}, {:boolean, destination.issue_confirmed_notifications}, EventTransitionBits.to_bitstring(destination.transitions) ] {:ok, params} end end @doc """ Parses a BACnet destination 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 case tags do [ {:bitstring, days}, {:time, from_time}, {:time, to_time}, recipient_raw, {:unsigned_integer, process_id}, {:boolean, issue_confirmed}, {:bitstring, event_trans} | rest ] -> with :ok <- if(ApplicationTags.valid_int?(process_id, 32), do: :ok, else: {:error, :invalid_process_identifier_value} ), {:ok, {recipient, _rest}} <- Recipient.parse([recipient_raw]) do dest = %__MODULE__{ recipient: recipient, process_identifier: process_id, issue_confirmed_notifications: issue_confirmed, transitions: EventTransitionBits.from_bitstring(event_trans), valid_days: DaysOfWeek.from_bitstring(days), from_time: from_time, to_time: to_time } {:ok, {dest, rest}} end _else -> {:error, :invalid_tags} end end @doc """ Validates whether the given BACnet destination is in form valid. It only validates the struct is valid as per type specification. """ @spec valid?(t()) :: boolean() def valid?( %__MODULE__{ recipient: %Recipient{} = recip, process_identifier: process_id, issue_confirmed_notifications: issue_confirmed, transitions: %EventTransitionBits{} = trans, valid_days: %DaysOfWeek{} = days, from_time: %BACnetTime{} = from_t, to_time: %BACnetTime{} = to_t } = _t ) when is_integer(process_id) and process_id >= 0 and process_id <= 4_294_967_295 and is_boolean(issue_confirmed) do Recipient.valid?(recip) and EventTransitionBits.valid?(trans) and DaysOfWeek.valid?(days) and BACnetTime.valid?(from_t) and BACnetTime.valid?(to_t) and BACnetTime.specific?(from_t) and BACnetTime.specific?(to_t) end def valid?(%__MODULE__{} = _t), do: false end