defmodule BACnet.Protocol.FaultParameters do @moduledoc """ BACnet fault parameters configure the conditions under which an object should report a FAULT event state (as opposed to NORMAL or OFFNORMAL). Fault algorithms are separate from event algorithms. While event algorithms detect alarm conditions, fault algorithms detect problems with the reliability of the input data or the object itself (e.g. sensor failure, out-of-range values that indicate hardware problems, etc.). This module provides encoding and parsing for all supported fault parameter types. The individual modules under this namespace contain the fields for each specific fault detection algorithm. ### Examples ```elixir # Fault out of range (common) iex> %FaultParameters.FaultCharacterString{ ...> fault_values: ["hello"] ...> } # Fault extended (vendor specific) iex> %FaultParameters.FaultExtended{ ...> vendor_id: 42, ...> extended_fault_type: 1, ...> parameters: [] ...> } ``` """ # TODO: Throw argument error in encode if not valid alias BACnet.Protocol.ApplicationTags alias BACnet.Protocol.Constants alias BACnet.Protocol.DeviceObjectPropertyRef alias BACnet.Protocol.FaultParameters.FaultCharacterString alias BACnet.Protocol.FaultParameters.FaultExtended alias BACnet.Protocol.FaultParameters.FaultLifeSafety alias BACnet.Protocol.FaultParameters.FaultState alias BACnet.Protocol.FaultParameters.FaultStatusFlags alias BACnet.Protocol.FaultParameters.None alias BACnet.Protocol.PropertyState @typedoc """ Possible BACnet fault parameters. """ @type fault_parameter :: None.t() | FaultCharacterString.t() | FaultExtended.t() | FaultLifeSafety.t() | FaultState.t() | FaultStatusFlags.t() @doc """ Encodes a fault parameter variant into BACnet application tag encoding. Used when configuring objects that support fault detection or when building event enrollment records that include fault parameters. """ @spec encode(fault_parameter(), Keyword.t()) :: {:ok, ApplicationTags.encoding()} | {:error, term()} def encode(fault_params, opts \\ []) def encode(%None{} = _params, _opts) do {:ok, {:constructed, {0, {:null, nil}, 0}}} end def encode(%FaultCharacterString{} = params, _opts) do with true <- is_list(params.fault_values) and Enum.all?(params.fault_values, &(is_binary(&1) and String.valid?(&1))), favalues when is_list(favalues) <- Enum.map(params.fault_values, &{:character_string, &1}) do {:ok, {:constructed, {1, [ constructed: {0, favalues, 0} ], 0}}} else false -> {:error, :invalid_params} # {:error, _err} = err -> err end end def encode(%FaultExtended{} = params, opts) do with true <- is_integer(params.vendor_id) and params.vendor_id >= 0 and params.vendor_id <= 65_535, true <- is_integer(params.extended_fault_type) and params.extended_fault_type >= 0, {:ok, vendor_id, _header} <- ApplicationTags.encode_value({:unsigned_integer, params.vendor_id}, opts), {:ok, extended_fault_type, _header} <- ApplicationTags.encode_value({:unsigned_integer, params.extended_fault_type}, opts) do {:ok, {:constructed, {2, [ tagged: {0, vendor_id, byte_size(vendor_id)}, tagged: {1, extended_fault_type, byte_size(extended_fault_type)}, constructed: {2, params.parameters, 0} ], 0}}} else false -> {:error, :invalid_params} {:error, _err} = err -> err end end def encode(%FaultLifeSafety{} = params, opts) do with true <- is_struct(params.mode, DeviceObjectPropertyRef), true <- is_list(params.fault_values) and Enum.all?(params.fault_values, &is_atom/1), {:ok, fault_values} <- Enum.reduce_while(params.fault_values, {:ok, []}, fn enum, {:ok, acc} -> case Constants.by_name(:life_safety_state, enum) do {:ok, val} -> {:cont, {:ok, [{:enumerated, val} | acc]}} :error -> {:halt, {:error, {:unknown_life_safety_fault_value, enum}}} end end), {:ok, mode} <- DeviceObjectPropertyRef.encode(params.mode, opts) do {:ok, {:constructed, {3, [ constructed: {0, fault_values, 0}, constructed: {1, mode, 0} ], 0}}} else false -> {:error, :invalid_params} {:error, _err} = err -> err end end def encode(%FaultState{} = params, _opts) do with true <- is_list(params.fault_values) and Enum.all?(params.fault_values, &is_struct(&1, PropertyState)), {:ok, fault_values} <- Enum.reduce_while(params.fault_values, {:ok, []}, fn enum, {:ok, acc} -> case PropertyState.encode(enum) do {:ok, [val]} -> {:cont, {:ok, [val | acc]}} {:error, _err} = err -> {:halt, {:error, err}} end end) do {:ok, {:constructed, {4, [ constructed: {0, Enum.reverse(fault_values), 0} ], 0}}} else false -> {:error, :invalid_params} {:error, _err} = err -> err end end def encode(%FaultStatusFlags{} = params, opts) do with true <- is_struct(params.status_flags, DeviceObjectPropertyRef), {:ok, flags} <- DeviceObjectPropertyRef.encode(params.status_flags, opts) do {:ok, {:constructed, {5, [ constructed: {0, flags, 0} ], 0}}} else false -> {:error, :invalid_params} {:error, _err} = err -> err end end @doc """ Parses a constructed application tag into the corresponding fault parameter struct. The inverse of `encode/2`. """ @spec parse(binary()) :: {:ok, fault_parameter()} | {:error, term()} def parse(fault_values_tag) # 0 = None def parse({:constructed, {0, fault_values_tags, 0}}) do case List.wrap(fault_values_tags) do [{:null, nil}] -> {:ok, %None{}} _term -> {:error, :invalid_fault_values} end end # 0 = None def parse({:tagged, {0, "", 0}}) do {:ok, %None{}} end # 1 = Fault Character String def parse({:constructed, {1, fault_values_tags, 0}}) do case List.wrap(fault_values_tags) do [ constructed: {0, strings, _length2} ] -> new_strings = Enum.reduce_while(List.wrap(strings), [], fn {:character_string, str}, acc -> {:cont, [str | acc]} _other, _acc -> {:halt, {:error, :invalid_fault_values}} end) case new_strings do favalues when is_list(favalues) -> fault = %FaultCharacterString{ fault_values: Enum.reverse(new_strings) } {:ok, fault} {:error, _err} = err -> err end _term -> {:error, :invalid_fault_values} end end # 2 = Extended def parse({:constructed, {2, fault_values_tags, 0}}) do case List.wrap(fault_values_tags) do [ tagged: {0, vendor_id_raw, _length}, tagged: {1, ext_fault_raw, _length2}, # TODO: May be not constructed (tagged) constructed: {2, parameters, _length3} ] -> with {:ok, {:unsigned_integer, vendor_id}} <- ApplicationTags.unfold_to_type(:unsigned_integer, vendor_id_raw), :ok <- if(ApplicationTags.valid_int?(vendor_id, 16), do: :ok, else: {:error, :invalid_vendor_id_value} ), {:ok, {:unsigned_integer, ext_fault}} <- ApplicationTags.unfold_to_type(:unsigned_integer, ext_fault_raw) do fault = %FaultExtended{ vendor_id: vendor_id, extended_fault_type: ext_fault, parameters: parameters } {:ok, fault} else {:error, _err} = err -> err end _term -> {:error, :invalid_fault_values} end end # 3 = Fault Life Safety def parse({:constructed, {3, fault_values_tags, 0}}) do case List.wrap(fault_values_tags) do [ constructed: {0, fault_values_raw, _length3}, constructed: {1, mode_raw, _length4} ] -> with {:ok, fault_values} <- Enum.reduce_while(fault_values_raw, {:ok, []}, fn pack, {:ok, acc} -> case ApplicationTags.unfold_to_type(:enumerated, pack) do {:ok, {:enumerated, value}} -> case Constants.by_value_with_reason( :life_safety_state, value, {:unknown_life_safety_fault_value, value} ) do {:ok, value_c} -> {:cont, {:ok, [value_c | acc]}} {:error, _err} = err -> {:halt, err} end term -> {:halt, term} end end), {:ok, {mode, _tags}} <- DeviceObjectPropertyRef.parse(mode_raw) do fault = %FaultLifeSafety{ mode: mode, fault_values: Enum.reverse(fault_values) } {:ok, fault} else {:error, _err} = err -> err end _term -> {:error, :invalid_fault_values} end end # 4 = Fault State def parse({:constructed, {4, fault_values_tags, 0}}) do case List.wrap(fault_values_tags) do [ constructed: {0, seq_propstates, _length2} ] -> case Enum.reduce_while(List.wrap(seq_propstates), {:ok, []}, fn term, {:ok, acc} -> case BACnet.Protocol.PropertyState.parse(List.wrap(term)) do {:ok, {state, _rest}} -> {:cont, {:ok, [state | acc]}} _term -> {:halt, {:error, :invalid_fault_values_parameter}} end end) do {:ok, fault_values} -> fault = %FaultState{ fault_values: Enum.reverse(fault_values) } {:ok, fault} {:error, _err} = err -> err end _term -> {:error, :invalid_fault_values} end end # 5 = Fault Status Flags def parse({:constructed, {5, fault_values_tags, 0}}) do case List.wrap(fault_values_tags) do [ constructed: {0, status_flags_ref_raw, _length2} ] -> case DeviceObjectPropertyRef.parse(status_flags_ref_raw) do {:ok, {status_flags_ref, _tags}} -> fault = %FaultStatusFlags{ status_flags: status_flags_ref } {:ok, fault} {:error, _err} = err -> err end _term -> {:error, :invalid_fault_values} end end def parse(_fault_values_tag) do {:error, :invalid_tag} end @doc """ Validates whether the given fault parameter is in form valid. It only validates the struct is valid as per type specification. """ @spec valid?(fault_parameter()) :: boolean() def valid?(t) for module <- [ FaultCharacterString, FaultExtended, FaultLifeSafety, FaultState, FaultStatusFlags, None ] do var = Macro.var(:t, __MODULE__) def valid?(%unquote(module){} = unquote(var)) do unquote(BACnet.BeamTypes.generate_valid_clause(module, __ENV__)) end end end