Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/name_value.ex
defmodule BACnet.Protocol.NameValue do
@moduledoc """
BACnetNameValue is a structure used primarily for the `Tags` property
(of type BACnetARRAY of BACnetNameValue) that exists on nearly every
standardized BACnet object type (ASHRAE 135-2016 Clause 12.x and Annex Y).
Each entry associates a name (CharacterString) with an optional value.
When the value is absent the tag is considered a "semantic tag".
To allow tag names to be concatenated,
tag names shall not contain semicolon characters (ASHRAE 135-2016 Y.1.4).
Simple tag names, e.g., `exhaust`, are reserved for definition by ASHRAE.
Tag names defined by organizations other than ASHRAE shall use a prefix to ensure uniqueness.
This prefix shall be either:
1) A reversed registered DNS name, followed by a period character. e.g., `com.example.`, or
2) A BACnet vendor identifier in decimal, followed by a dash character. e.g., `555-`
The value, when present, is limited to any primitive BACnet datatype.
### ASN.1 (ASHRAE 135-2016)
BACnetNameValue ::= SEQUENCE {
name [0] CharacterString,
value ABSTRACT-SYNTAX.&Type OPTIONAL
-- value is limited to primitive datatypes
}
"""
alias BACnet.Protocol.ApplicationTags
alias BACnet.Protocol.ApplicationTags.Encoding
alias BACnet.Protocol.Utility
import Utility, only: [pattern_extract_tags: 4]
@typedoc """
A single BACnetNameValue entry.
Only primitive `Encoding` is allowed as value,
otherwise the value is absent (`nil`).
"""
@type t :: %__MODULE__{
name: String.t(),
value: Encoding.t() | nil
}
@fields [:name, :value]
@enforce_keys @fields
defstruct @fields
@doc """
Parses a BACnetNameValue from application tags encoding (context tagged elements).
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term()}
def parse(tags) when is_list(tags) do
with {:ok, name, rest} <-
pattern_extract_tags(tags, {:tagged, {0, _val, _len}}, :character_string, false),
{:ok, {value, rest}} <-
(case rest do
[value | rest] ->
# Some BACnet devices (i.e. Delta Controls) put the value into encoding tag 1,
# despite the BACnet spec being clear that there's no tag,
# even since the introduction in 135-2016 - so we'll be permissive
value0 =
case value do
{:constructed, {1, tag, 0}} -> tag
_other -> value
end
case Encoding.create(value0) do
{:ok, %Encoding{encoding: :primitive} = enc} -> {:ok, {enc, rest}}
{:ok, _other} -> {:error, :invalid_value}
other -> other
end
_other ->
{:ok, {nil, rest}}
end) do
nv = %__MODULE__{name: name, value: value}
{:ok, {nv, rest}}
else
{:error, :missing_pattern} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@doc """
Encodes a BACnetNameValue into application tags encoding.
"""
@spec encode(t(), Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{name: name, value: value}, _opts \\ []) do
with true <- is_binary(name) and byte_size(name) > 0,
{:ok, name_tag} <- ApplicationTags.create_tag_encoding(0, :character_string, name) do
case value do
nil ->
{:ok, [name_tag]}
%Encoding{encoding: :primitive} ->
with {:ok, value_tag} <- Encoding.to_encoding(value) do
{:ok, [name_tag, value_tag]}
end
_other ->
{:error, :invalid_value}
end
else
false -> {:error, :invalid_name}
{:error, _err} = err -> err
end
end
@doc """
Returns true if the NameValue struct contains a valid name and (optionally) a
well-formed value of a supported type.
"""
@spec valid?(t()) :: boolean()
def valid?(%__MODULE__{name: name, value: value} = _t)
when is_binary(name) and byte_size(name) > 0 do
# ASHRAE Y.1.4 specifies:
# "To allow tag names to be concatenated, tag names shall not contain semicolon characters."
not String.contains?(name, ";") and
case value do
nil -> true
%Encoding{encoding: :primitive} -> true
_other -> false
end
end
def valid?(%__MODULE__{} = _tag), do: false
end