Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/access_specification/property.ex
defmodule BACnet.Protocol.AccessSpecification.Property do
# TODO: Docs
# if property_value = nil, then read access specification
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.ApplicationTags.Encoding
alias BACnet.Protocol.Constants
import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4]
require Constants
@type t :: %__MODULE__{
property_identifier: Constants.property_identifier() | non_neg_integer(),
property_array_index: non_neg_integer() | nil,
property_value: Encoding.t() | nil
}
@fields [
:property_identifier,
:property_array_index,
:property_value
]
@enforce_keys @fields
defstruct @fields
@doc """
Encodes a BACnet Read/Write Access Specification Property into BACnet application tags encoding.
"""
@spec encode(t() | :all | :required | :optional, Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(property, opts \\ [])
def encode(property, _opts) when property in [:all, :required, :optional] do
with {:ok, property, _header} <-
ApplicationTags.encode_value(
{:enumerated, Constants.by_name!(:property_identifier, property)}
) do
{:ok, [{:tagged, {0, property, byte_size(property)}}]}
end
end
def encode(%__MODULE__{} = property, _opts) do
with {:ok, property_identifier, _header} <-
ApplicationTags.encode_value(
{:enumerated,
Constants.by_name_atom(:property_identifier, property.property_identifier)}
),
{:ok, array_index} <-
(if property.property_array_index do
case ApplicationTags.encode_value(
{:unsigned_integer, property.property_array_index}
) do
{:ok, array_index, _header} ->
{:ok, {:tagged, {1, array_index, byte_size(array_index)}}}
term ->
term
end
else
{:ok, nil}
end),
{:ok, prop_value} <-
(if property.property_value do
with {:ok, base} <- Encoding.to_encoding(property.property_value) do
{:ok, {:constructed, {2, base, 0}}}
end
else
{:ok, nil}
end) do
params = [
{:tagged, {0, property_identifier, byte_size(property_identifier)}},
array_index,
prop_value
]
{:ok, Enum.reject(params, &is_nil/1)}
end
end
@doc """
Parses a BACnet Read/Write Access Specification Property from BACnet application tags encoding.
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t() | :all | :required | :optional, rest :: ApplicationTags.encoding_list()}}
| {:error, term()}
def parse(tags) when is_list(tags) do
with {:ok, property_identifier, rest} <-
pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :enumerated, false),
{:ok, property_array_index, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _t, _l}}, :unsigned_integer, true),
{:ok, prop_value, rest} <-
pattern_extract_tags(rest, {:constructed, {2, _t, _l}}, nil, true) do
item =
case Constants.by_value(
:property_identifier,
property_identifier,
property_identifier
) do
propident when propident in [:all, :required, :optional] ->
propident
propident ->
%__MODULE__{
property_identifier: propident,
property_array_index: property_array_index,
property_value:
case prop_value do
{:constructed, {2, tag, _len}} -> Encoding.create!(tag)
_else -> nil
end
}
end
{:ok, {item, rest}}
else
{:error, :missing_pattern} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@doc """
Validates whether the given access specification property is in form valid.
It only validates the struct is valid as per type specification.
Be aware, this function does not know whether it is a read or
write access specification, thus it can't verify if the special
property identifiers (atoms) are as per BACnet specification.
Only read supports the special property identifiers.
"""
@spec valid?(t()) :: boolean()
def valid?(
%__MODULE__{
property_identifier: prop_identifier,
property_array_index: array_index
} = _t
) do
(Constants.has_by_name(:property_identifier, prop_identifier) or
(is_integer(prop_identifier) and prop_identifier >= 0 and
prop_identifier <= Constants.macro_by_name(:asn1, :max_instance_and_property_id))) and
(is_nil(array_index) or (is_integer(array_index) and array_index >= 0))
end
end