Packages

BACstack is a low-level Elixir implementation for the ASHRAE standard 135, BACnet - Building Automation and Controller network. In its current published state, v0.0.x, it can (only) act as a BACnet client for other BACnet devices.

Current section

Files

Jump to
bacstack lib bacnet protocol event_algorithms command_failure.ex
Raw

lib/bacnet/protocol/event_algorithms/command_failure.ex

defmodule BACnet.Protocol.EventAlgorithms.CommandFailure do
@moduledoc """
Implements the BACnet event algorithm `CommandFailure`.
The CommandFailure event algorithm detects whether the monitored value and the feedback value
disagree for a time period. It may be used, for example, to verify that a process change
has occurred after writing a property.
For more specific information about the event algorithm, consult ASHRAE 135 13.3.4.
"""
alias BACnet.Protocol.ApplicationTags.Encoding
alias BACnet.Protocol.Constants
alias BACnet.Protocol.EventParameters.CommandFailure, as: Params
alias BACnet.Protocol.NotificationParameters.CommandFailure, 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, Encoding.t(), 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, Encoding.t(), required: false)
end
@doc """
Creates a new algorithm state.
The `encoding` field of `Encoding` structs is ignored.
"""
@spec new(Encoding.t(), Params.t()) :: t()
def new(%Encoding{} = monitored_value, %Params{} = params) do
unless monitored_value.type == params.feedback_value.type or
typeof(monitored_value.value) == typeof(params.feedback_value.value) do
raise ArgumentError,
"Expected monitored value and feedback value (from params) " <>
"have the same type or value datatype, got: " <>
"monitored_value=#{inspect(monitored_value)}, " <>
"params=#{inspect(params)}"
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 pFeedbackValue is not equal to
> pMonitoredValue for pTimeDelay, then indicate a transition to
> the OFFNORMAL event state.
>
> (b) If pCurrentState is OFFNORMAL, and pMonitoredValue is equal to
> pMonitoredValue for pTimeDelayNormal, then indicate a transition to
> the NORMAL 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)
offnormal =
if state.monitored_value.value != state.parameters.feedback_value.value,
do: state.monitored_value.value
offnormal_event = current_normal 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)
current_normal ->
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_struct(value, Encoding) and
(value.type == state.parameters.feedback_value.type or
typeof(value.value) == typeof(state.parameters.feedback_value.value)) do
raise ArgumentError,
"Expected a Encoding struct for monitored_value " <>
"with matching types for the monitored_value and parameters feedback_value" <>
", got: #{inspect(value)}"
end
%{acc | monitored_value: value}
{:parameters, value}, acc ->
unless is_struct(value, Params) and
(state.monitored_value.type == value.feedback_value.type or
typeof(state.monitored_value.value) == typeof(value.feedback_value.value)) do
raise ArgumentError,
"Expected EventParameters.CommandFailure struct for params " <>
"with matching types for feedback_value and the monitored_value" <>
", 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{
command_value: state.monitored_value,
feedback_value: state.parameters.feedback_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 typeof(term()) :: atom()
defp typeof(a) do
cond do
is_atom(a) -> :atom
is_binary(a) -> :binary
is_bitstring(a) -> :bitstring
is_boolean(a) -> :boolean
is_float(a) -> :float
is_integer(a) -> :integer
is_tuple(a) -> :tuple
true -> raise ArgumentError, "Incomplete typeof list - unknown type - got: #{inspect(a)}"
end
end
end