Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/group_channel_value.ex
defmodule BACnet.Protocol.GroupChannelValue do
# TODO: Docs
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4]
@typedoc """
Represents a BACnet Group Channel Value.
"""
@type t :: %__MODULE__{
channel: ApplicationTags.unsigned16(),
overriding_priority: 1..16 | nil,
value:
ApplicationTags.Encoding.t() | (lightning_command :: [ApplicationTags.Encoding.t()])
}
@fields [
:channel,
:overriding_priority,
:value
]
@enforce_keys @fields
defstruct @fields
@doc """
Encodes a group channel value struct into application tags encoding.
"""
@spec encode(t(), Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{} = value, _opts \\ []) do
with :ok <-
if(ApplicationTags.valid_int?(value.channel, 16),
do: :ok,
else: {:error, :invalid_channel_value}
),
{:ok, channel, _header} <-
ApplicationTags.encode_value({:unsigned_integer, value.channel}),
{:ok, priority} <-
(if value.overriding_priority do
ApplicationTags.create_tag_encoding(
1,
{:unsigned_integer, value.overriding_priority}
)
else
{:ok, nil}
end),
{:ok, val} <-
(if is_list(value.value) do
with {:ok, list} <- encode_list_value(value) do
{:ok, {:constructed, {0, list, 0}}}
end
else
ApplicationTags.Encoding.to_encoding(value.value)
end) do
params = [
{:tagged, {0, channel, byte_size(channel)}},
priority,
val
]
{:ok, Enum.reject(params, &is_nil/1)}
else
{:error, _err} = err -> err
end
end
@doc """
Parses a BACnet group channel value 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, channel, rest} <-
pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :unsigned_integer, false),
:ok <-
if(ApplicationTags.valid_int?(channel, 16),
do: :ok,
else: {:error, :invalid_channel_value}
),
{:ok, priority, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _t, _l}}, :unsigned_integer, true),
{:ok, {value, rest}} <- parse_value(rest) do
# TODO: Implement BACnetLightningCommand
chanvalue = %__MODULE__{
channel: channel,
overriding_priority: priority,
value: value
}
{:ok, {chanvalue, rest}}
else
{:error, :missing_pattern} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@doc """
Validates whether the given BACnet group channel value is in form valid.
It only validates the struct is valid as per type specification.
"""
@spec valid?(t()) :: boolean()
def valid?(t)
def valid?(
%__MODULE__{
channel: channel,
overriding_priority: priority,
value: %ApplicationTags.Encoding{}
} = _t
)
when (is_nil(priority) or priority in 1..16) and is_integer(channel) and channel >= 0 and
channel <= 65_535,
do: true
def valid?(
%__MODULE__{
channel: channel,
overriding_priority: priority,
value: value
} = _t
)
when (is_nil(priority) or priority in 1..16) and is_integer(channel) and channel >= 0 and
channel <= 65_535 and
is_list(value) do
Enum.all?(value, &is_struct(&1, ApplicationTags.Encoding))
end
def valid?(%__MODULE__{} = _t), do: false
@spec parse_value(ApplicationTags.encoding_list()) ::
{:ok, {[ApplicationTags.Encoding.t()], rest :: ApplicationTags.encoding_list()}}
| {:error, term()}
defp parse_value([{type, _value} = tag | rest])
when type in [
:null,
:boolean,
:enumerated,
:unsigned_integer,
:integer,
:real,
:double,
:date,
:time,
:octet_string,
:character_string,
:bitstring,
:object_identifier
] do
{:ok, {ApplicationTags.Encoding.create!(tag), rest}}
end
defp parse_value([{:constructed, {0, value, _len}} | rest]) do
{:ok, {Enum.map(value, &ApplicationTags.Encoding.create!/1), rest}}
end
defp parse_value(_tag), do: {:error, :invalid_tags}
defp encode_list_value(%__MODULE__{} = value) do
result =
Enum.reduce_while(value.value, {:ok, []}, fn val, {:ok, acc} ->
case ApplicationTags.Encoding.to_encoding(val) do
{:ok, enc} -> {:cont, {:ok, [enc | acc]}}
term -> {:halt, term}
end
end)
case result do
{:ok, list} ->
new_list =
list
|> Enum.reverse()
|> List.flatten()
{:ok, new_list}
term ->
term
end
end
end