Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/cov_subscription.ex
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