Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/prescale.ex
defmodule BACnet.Protocol.Prescale do
@moduledoc """
A Prescale is a simple multiplier / modulo-divide pair used by Accumulator objects
to scale the raw pulse count coming from a physical meter into the engineering units
that the present value property should report.
The multiplier is applied first, then the result is divided by the modulo_divide
value (the remainder is discarded). This two-stage scaling allows devices to
represent both fractional pulses (for example, a meter that produces one pulse
per 0.1 kWh) and to handle situations where the display on the physical meter
rolls over at a different point than the internal counter.
The structure exists because many real-world utility meters do not produce
pulses that directly correspond to the desired engineering units. By exposing
the scaling factors as readable and writable properties, BACnet makes it
possible to commission or reconfigure pulse scaling without changing firmware.
### Examples (Doc Test)
```elixir
iex> scale = %Prescale{multiplier: 10, modulo_divide: 1000}
iex> scale.multiplier
10
```
"""
# TODO: Throw argument error in encode if not valid
alias BACnet.Protocol.ApplicationTags
import BACnet.Protocol.Utility, only: [pattern_extract_tags: 4]
@typedoc """
Represents the prescaling factors used by Accumulator objects.
"""
@type t :: %__MODULE__{
multiplier: non_neg_integer(),
modulo_divide: non_neg_integer()
}
@fields [
:multiplier,
:modulo_divide
]
@enforce_keys @fields
defstruct @fields
@doc """
Encodes a BACnet prescale into application tags encoding.
"""
@spec encode(t(), Keyword.t()) ::
{:ok, ApplicationTags.encoding_list()} | {:error, term()}
def encode(%__MODULE__{} = prescale, opts \\ []) do
with {:ok, multiplier, _header} <-
ApplicationTags.encode_value({:unsigned_integer, prescale.multiplier}, opts),
{:ok, modulo, _header} <-
ApplicationTags.encode_value({:unsigned_integer, prescale.modulo_divide}, opts) do
{:ok,
[
tagged: {0, multiplier, byte_size(multiplier)},
tagged: {1, modulo, byte_size(modulo)}
]}
else
{:error, _err} = err -> err
end
end
@doc """
Parses a BACnet prescale from application tags encoding.
"""
@spec parse(ApplicationTags.encoding_list()) ::
{:ok, {t(), rest :: ApplicationTags.encoding_list()}} | {:error, term()}
def parse(tags) when is_list(tags) do
with {:ok, multiplier, rest} <-
pattern_extract_tags(tags, {:tagged, {0, _t, _l}}, :unsigned_integer, false),
{:ok, modulo, rest} <-
pattern_extract_tags(rest, {:tagged, {1, _t, _l}}, :unsigned_integer, false) do
prescale = %__MODULE__{
multiplier: multiplier,
modulo_divide: modulo
}
{:ok, {prescale, rest}}
else
{:error, :missing_pattern} -> {:error, :invalid_tags}
{:error, _err} = err -> err
end
end
@doc """
Validates whether the given prescale is in form valid.
It only validates the struct is valid as per type specification.
"""
@spec valid?(t()) :: boolean()
def valid?(
%__MODULE__{
multiplier: multiplier,
modulo_divide: modulo_divide
} = _t
)
when is_integer(multiplier) and multiplier >= 0 and is_integer(modulo_divide) and
modulo_divide >= 0,
do: true
def valid?(%__MODULE__{} = _t), do: false
end