Current section
Files
Jump to
Current section
Files
lib/bacnet/protocol/event_algorithms/change_of_bitstring.ex
defmodule BACnet.Protocol.EventAlgorithms.ChangeOfBitstring do
@moduledoc """
Implements the BACnet event algorithm `ChangeOfBitstring`.
The ChangeOfBitstring event algorithm detects whether the monitored value of type
BITSTRING equals a value that is listed as an alarm value, after applying a bitmask.
For more specific information about the event algorithm, consult ASHRAE 135 13.3.1.
"""
alias BACnet.Protocol.Constants
alias BACnet.Protocol.EventParameters.ChangeOfBitstring, as: Params
alias BACnet.Protocol.NotificationParameters.ChangeOfBitstring, as: Notify
alias BACnet.Protocol.StatusFlags
import BACnet.Macro
require Constants
@typedoc """
Representative type for the event algorithm.
"""
opaquedstruct do
field(:current_state, Constants.event_state(), required: true)
field(:monitored_value, tuple(), required: true)
field(:status_flags, StatusFlags.t(), required: true)
field(:parameters, Params.t(), required: true)
field(:dt_normal, DateTime.t() | nil, required: true)
field(:dt_offnormal, DateTime.t() | nil, required: true)
field(:last_value, tuple(), required: false)
end
@doc """
Creates a new algorithm state.
"""
@spec new(tuple(), Params.t()) :: t()
def new(monitored_value, %Params{} = params) when is_tuple(monitored_value) do
unless Enum.all?(Tuple.to_list(monitored_value), &is_boolean/1) do
raise ArgumentError,
"Expected a tuple of booleans for monitored_value, got: #{inspect(monitored_value)}"
end
%__MODULE__{
current_state: Constants.macro_assert_name(:event_state, :normal),
monitored_value: monitored_value,
status_flags: StatusFlags.from_bitstring({false, false, false, false}),
parameters: params,
dt_normal: nil,
dt_offnormal: nil,
last_value: nil
}
end
@doc """
Calculates the new state for the current state and parameters.
Prior to this function invocation, the state should have been
updated with `update/2`, if any of the properties has changed.
`:delayed_event` helps identifying whether the algorithm needs
to be called periodically in order to overcome the `time_delay`
and trigger a state change. As soon as `:event` or `:no_event`
is given as flag, it means the caller can go back to event
orientated calling.
The `status_flags` field of the notifications parameters is
updated from the state with the correct `in_alarm` state,
however to ensure the Status Flags have an overall correct status,
the user has to make sure all bits are correctly.
ASHRAE 135:
> The conditions evaluated by this event algorithm are:
>
> (a) If pCurrentState is NORMAL, and pMonitoredValue, after applying pBitmask,
> is equal to any of the values contained in pAlarmValues for pTimeDelay,
> then indicate a transition to the OFFNORMAL event state.
>
> (b) If pCurrentState is OFFNORMAL, and pMonitoredValue, after applying pBitmask,
> is not equal to any of the values contained in pAlarmValues for pTimeDelayNormal,
> then indicate a transition to the NORMAL event state.
>
> (c) Optional: If pCurrentState is OFFNORMAL, and pMonitoredValue, after applying pBitmask,
> is equal to one of the values contained in pAlarmValues that is different from the value
> that caused the last transition to OFFNORMAL and remains equal to that value for pTimeDelay,
> then indicate a transition to the OFFNORMAL event state.
"""
@spec execute(t()) ::
{:event, new_state :: t(), Notify.t()}
| {:delayed_event | :no_event, new_state :: t()}
def execute(%__MODULE__{} = state) do
current_normal = state.current_state == Constants.macro_assert_name(:event_state, :normal)
bitmasked =
Bitwise.band(tuple_to_int(state.monitored_value), tuple_to_int(state.parameters.bitmask))
offnormal = Enum.find(state.parameters.alarm_values, &(tuple_to_int(&1) == bitmasked))
offnormal_base_cond = current_normal or state.last_value != offnormal
offnormal_event = offnormal_base_cond and offnormal != nil
normal_event = not current_normal and offnormal == nil
offnormal_dt =
cond do
offnormal_event and state.parameters.time_delay > 0 ->
state.dt_offnormal || get_dt(state.parameters.time_delay)
offnormal_base_cond ->
state.dt_offnormal
true ->
nil
end
normal_dt =
cond do
normal_event and (state.parameters.time_delay_normal || state.parameters.time_delay) > 0 ->
state.dt_normal ||
get_dt(state.parameters.time_delay_normal || state.parameters.time_delay)
not current_normal ->
state.dt_normal
true ->
nil
end
current_dt = get_dt(0)
{event, new_event_state} =
cond do
offnormal_event and offnormal_dt == nil ->
{true, Constants.macro_assert_name(:event_state, :offnormal)}
offnormal_event and DateTime.compare(current_dt, offnormal_dt) != :lt ->
{true, Constants.macro_assert_name(:event_state, :offnormal)}
normal_event and normal_dt == nil ->
{true, Constants.macro_assert_name(:event_state, :normal)}
normal_event and DateTime.compare(current_dt, normal_dt) != :lt ->
{true, Constants.macro_assert_name(:event_state, :normal)}
true ->
{false, state.current_state}
end
new_state = %__MODULE__{
state
| current_state: new_event_state,
dt_normal: unless(event, do: normal_dt),
dt_offnormal: unless(event, do: offnormal_dt),
last_value: if(event, do: offnormal, else: state.last_value)
}
compute_return(event, state, new_state)
end
@doc """
Updates the state using the given parameters (`monitored_value`, `parameters`, `status_flags`).
"""
@spec update(t(), Keyword.t()) :: t()
def update(%__MODULE__{} = state, params) when is_list(params) do
unless Keyword.keyword?(params) do
raise ArgumentError, "Expected a keyword list as argument, got: #{inspect(params)}"
end
Enum.reduce(params, state, fn
{:monitored_value, value}, acc ->
unless is_tuple(value) and Enum.all?(Tuple.to_list(value), &is_boolean/1) do
raise ArgumentError,
"Expected a tuple of booleans for monitored_value, got: #{inspect(value)}"
end
%{acc | monitored_value: value}
{:parameters, value}, acc ->
unless is_struct(value, Params) do
raise ArgumentError,
"Expected EventParameters.ChangeOfBitstring struct for params, got: #{inspect(value)}"
end
%{acc | parameters: value}
{:status_flags, value}, acc ->
unless is_struct(value, StatusFlags) do
raise ArgumentError,
"Expected StatusFlags struct for status_flags, got: #{inspect(value)}"
end
%{acc | status_flags: value}
{key, _value}, _acc ->
raise ArgumentError, "Unknown key #{key}"
end)
end
@spec compute_return(boolean(), t(), t()) ::
{:event, t(), Notify.t()} | {:delayed_event | :no_event, t()}
defp compute_return(event, old_state, new_state)
defp compute_return(
true,
%__MODULE__{} = _state,
%__MODULE__{current_state: state2} = state
) do
notify = %Notify{
referenced_bitstring: state.monitored_value,
status_flags: %{
state.status_flags
| in_alarm: state2 != Constants.macro_assert_name(:event_state, :normal)
}
}
{:event, state, notify}
end
defp compute_return(
false,
%__MODULE__{} = _state,
%__MODULE__{dt_normal: dtn, dt_offnormal: dto} = state
)
when dtn != nil or dto != nil,
do: {:delayed_event, state}
defp compute_return(false, _state, state), do: {:no_event, state}
@spec get_dt(non_neg_integer()) :: DateTime.t()
defp get_dt(0), do: DateTime.now!(Application.get_env(:bacstack, :default_timezone, "Etc/UTC"))
defp get_dt(offset), do: DateTime.add(get_dt(0), offset, :second)
@spec tuple_to_int(tuple()) :: non_neg_integer()
defp tuple_to_int(tuple) when is_tuple(tuple) do
BACnet.Internal.tuple_to_int(tuple)
end
end