defmodule BACnet.Protocol.CovSubscription do @moduledoc """ A COV Subscription records that a particular client (identified by its Process Identifier and Recipient address) has asked to be notified whenever a specific property (or the entire object) changes value beyond a configured increment. The structure stores the subscription parameters that the server must remember: the recipient address (which may be a local process or a remote device), the monitored property reference, the COV increment, the issue confirmed flag, and the lifetime of the subscription. When the monitored value changes sufficiently, the server walks its subscription table and sends ConfirmedCOVNotification or UnconfirmedCOVNotification messages to every active subscriber. Proper management of these records (including automatic removal when the lifetime expires) is one of the more complex pieces of state that a BACnet server must maintain. ### Examples (Doc Test) ```elixir iex> sub = %CovSubscription{ ...> recipient: %Recipient{type: :device, device: %ObjectIdentifier{type: :device, instance: 100}, address: nil}, ...> recipient_process: 1, ...> monitored_object_property: %ObjectPropertyRef{ ...> object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1}, ...> property_identifier: :present_value, ...> property_array_index: nil ...> }, ...> issue_confirmed_notifications: true, ...> time_remaining: 3600, ...> cov_increment: 0.5 ...> } iex> sub.time_remaining 3600 ``` """ # TODO: Throw argument error in encode if not valid alias BACnet.Protocol.ApplicationTags alias BACnet.Protocol.ObjectPropertyRef alias BACnet.Protocol.Recipient import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4] @typedoc """ Represents a Change of Value (COV) subscription. """ @type t :: %__MODULE__{ recipient: Recipient.t(), recipient_process: non_neg_integer(), monitored_object_property: ObjectPropertyRef.t(), issue_confirmed_notifications: boolean(), time_remaining: non_neg_integer(), cov_increment: float() | nil } @fields [ :recipient, :recipient_process, :monitored_object_property, :issue_confirmed_notifications, :time_remaining, :cov_increment ] @enforce_keys @fields defstruct @fields @doc """ Encodes a COV subscription struct into BACnet application tags encoding. """ @spec encode(t(), Keyword.t()) :: {:ok, ApplicationTags.encoding_list()} | {:error, term()} def encode(%__MODULE__{} = cov, opts \\ []) do with {:ok, [recipient]} <- Recipient.encode(cov.recipient, opts), {:ok, process_id, _header} <- ApplicationTags.encode_value({:unsigned_integer, cov.recipient_process}, opts), {:ok, obj_prop} <- ObjectPropertyRef.encode(cov.monitored_object_property, opts), {:ok, issue_confirmed} <- ApplicationTags.create_tag_encoding(2, {:boolean, cov.issue_confirmed_notifications}), {:ok, time_remaining} <- ApplicationTags.create_tag_encoding(3, {:unsigned_integer, cov.time_remaining}), {:ok, cov_increment} <- (if cov.cov_increment do ApplicationTags.create_tag_encoding(4, {:real, cov.cov_increment}) else {:ok, nil} end) do tags = [ {:constructed, {0, [ constructed: {0, recipient, 0}, tagged: {1, process_id, byte_size(process_id)} ], 0}}, {:constructed, {1, obj_prop, 0}}, issue_confirmed, time_remaining, cov_increment ] {:ok, Enum.reject(tags, &is_nil/1)} else {:error, _err} = err -> err end end @doc """ Parses a BACnet COV subscription application tags encoding into a struct. """ @spec parse(ApplicationTags.encoding_list()) :: {:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term()} def parse(tags) when is_list(tags) do with {:ok, {:constructed, {0, [ constructed: {0, recipient_raw, _len}, tagged: {1, process_identifier_raw, _len2} ], _len3}}, rest} <- pattern_extract_tags(tags, {:constructed, {0, _c, _l}}, nil, false), {:ok, {recipient, _rest}} <- Recipient.parse([recipient_raw]), {:ok, {:unsigned_integer, process_id}} <- ApplicationTags.unfold_to_type(:unsigned_integer, process_identifier_raw), {:ok, {:constructed, {1, objpropref_raw, _len}}, rest} <- pattern_extract_tags(rest, {:constructed, {1, _c, _l}}, nil, false), {:ok, {objpropref, _rest}} <- ObjectPropertyRef.parse(objpropref_raw), {:ok, issue_confirmed, rest} <- pattern_extract_tags(rest, {:tagged, {2, _c, _l}}, :boolean, false), {:ok, time_remaining, rest} <- pattern_extract_tags(rest, {:tagged, {3, _c, _l}}, :unsigned_integer, false), {:ok, cov_increment, rest} <- pattern_extract_tags(rest, {:tagged, {4, _c, _l}}, :real, true) do cov = %__MODULE__{ recipient: recipient, recipient_process: process_id, monitored_object_property: objpropref, issue_confirmed_notifications: issue_confirmed, time_remaining: time_remaining, cov_increment: cov_increment } {:ok, {cov, rest}} else {:ok, {:constructed, _term}, _rest} -> {:error, :invalid_tags} {:error, :missing_pattern} -> {:error, :invalid_tags} {:error, _err} = err -> err end end @doc """ Validates whether the given COV subscription is in form valid. It only validates the struct is valid as per type specification. """ @spec valid?(t()) :: boolean() def valid?( %__MODULE__{ recipient: %Recipient{} = recipient, recipient_process: process_id, monitored_object_property: %ObjectPropertyRef{} = ref, issue_confirmed_notifications: issue_confirmed, time_remaining: time_remaining, cov_increment: cov_increment } = _t ) when is_integer(process_id) and process_id >= 0 and process_id <= 4_294_967_295 and is_boolean(issue_confirmed) and is_integer(time_remaining) and time_remaining >= 0 and (is_nil(cov_increment) or is_float(cov_increment)) do Recipient.valid?(recipient) and ObjectPropertyRef.valid?(ref) end def valid?(%__MODULE__{} = _t), do: false end