defmodule BACnet.Protocol.PriorityArray do @moduledoc """ The Priority Array is a BACnet means of ensuring Command Prioritization. The Priority Array is an array (or in this case a struct) that contains 16 levels of priority, each which can take a particular value (each priority must have the same type) or NULL (`nil`). The highest priority (lowest array index) with a non-NULL value is the active command. ### BACnet Specification References - **ASN.1** (Clause 21): `BACnetPriorityArray ::= SEQUENCE SIZE (16) OF BACnetPriorityValue` (accessed as a `BACnetARRAY`). - **Commandable properties** (Clause 19.2): Every object type that has commandable properties (Present_Value on Output and Value objects, etc.) shall have a corresponding `priority_array` property of this type. - The 16 priority levels are fixed and global across the BACnet internetwork. ### Command Prioritization In building control systems, an object may be manipulated by a number of entities. For example, the present value of a Binary Output object may be set by several applications, such as demand metering, optimum start/stop, etc. Each such application program has a well-defined function it needs to perform. When the actions of two or more application programs conflict with regard to the value of a property, there is a need to arbitrate between them. The objective of the arbitration process is to ensure the desired behavior of an object that is manipulated by several program (or non-program) entities. For example, a start/stop program may specify that a particular Binary Output should be ON, while demand metering may specify that the same Binary Output should be OFF. In this case, the OFF should take precedence. An operator may be able to override the demand metering program and force the Binary Output ON, in which case the ON should take precedence. In BACnet, this arbitration is provided by a prioritization scheme that assigns varying levels of priorities to commanding entities on a system-wide basis. Each object that contains a commandable property is responsible for acting upon prioritized commands in the order of their priorities. While there is a trade-off between the complexity and the robustness of any such mechanism, the scheme described here is intended to be effective but applicable to even simple BACnet devices. ### Prioritization Mechanism For BACnet objects, commands are prioritized based upon a fixed number of priorities that are assigned to command-issuing entities. A prioritized command (one that is directed at a commandable property of an object) is performed via a WriteProperty service request or a WritePropertyMultiple service request. The request primitive includes a conditional 'Priority' parameter that ranges from 1 to 16. Each commandable property of an object has an associated priority table that is represented by a `priority_array` property. The PriorityArray consists of an array of commanded values in order of decreasing priority. The first value in the array corresponds to priority 1 (highest), the second value corresponds to priority 2, and so on, to the sixteenth value that corresponds to priority 16 (lowest). An entry in the PriorityArray may have a commanded value or a NULL. A NULL value indicates that there is no existing command at that priority. An object continuously monitors all entries within the priority table in order to locate the entry with the highest priority non-NULL value and sets the commandable property to this value. A commanding entity (application program, operator, etc.) may issue a command to write to the commandable property of an object, or it may relinquish a command issued earlier. Relinquishing of a command is performed by a write operation similar to the command itself, except that the commandable property value is NULL. Relinquishing a command places a NULL value in the PriorityArray corresponding to the appropriate priority. This prioritization approach shall be applied to local actions that change the value of commandable properties as well as to write operations via BACnet services. ### Examples ```elixir iex> pa = %PriorityArray{priority_1: 99.0, priority_8: 21.5} iex> PriorityArray.get_value(pa) {1, 99.0} iex> pa = %{pa | priority_1: nil} iex> PriorityArray.get_value(pa) {8, 21.5} ``` #### Edge cases Empty array (all priorities relinquished): ```elixir iex> empty = %PriorityArray{} iex> PriorityArray.get_value(empty) nil ``` From list with leading nils (lower priorities win only if higher are nil): ```elixir iex> pa = PriorityArray.from_list([nil, nil, 55.0]) iex> PriorityArray.get_value(pa) {3, 55.0} ``` ### See Also - `BACnet.Protocol.BACnetArray` - `BACnet.Protocol.ActionCommand` - `BACnet.Protocol.SetpointReference` """ alias BACnet.BeamTypes alias BACnet.Protocol.BACnetArray @typedoc """ Base type for the BACnet Priority Array. """ @type t :: t(term()) @typedoc """ Represents the BACnet Priority Array with 16 priorities. The lowest priority number has the highest priority. If a priority is nil, the priority is unset. """ @type t(subtype) :: %__MODULE__{ priority_1: subtype | nil, priority_2: subtype | nil, priority_3: subtype | nil, priority_4: subtype | nil, priority_5: subtype | nil, priority_6: subtype | nil, priority_7: subtype | nil, priority_8: subtype | nil, priority_9: subtype | nil, priority_10: subtype | nil, priority_11: subtype | nil, priority_12: subtype | nil, priority_13: subtype | nil, priority_14: subtype | nil, priority_15: subtype | nil, priority_16: subtype | nil } @fields [ :priority_1, :priority_2, :priority_3, :priority_4, :priority_5, :priority_6, :priority_7, :priority_8, :priority_9, :priority_10, :priority_11, :priority_12, :priority_13, :priority_14, :priority_15, :priority_16 ] # We do not need to enforce keys, as default nil is valid defstruct @fields @priorities Enum.to_list(1..16//1) @doc """ Fetches the value for a specific `key` in the given `array`. `key` is the priority number or the struct field name. This function is implemented for the `Access` behaviour and allows to access the fields using the priority number (or the atom key). This function will raise for an invalid atom key. """ @spec fetch(t(subtype), 1..16 | atom()) :: {:ok, subtype | nil} when subtype: var def fetch(%__MODULE__{} = array, key) when key in 1..16 or key in @fields do Map.fetch(array, key_to_struct_key(key)) end @doc """ Create a Priority Array from a BACnetArray. The types of the array values are not checked. However, they should all be the same. The size of the array must be 16 or smaller (lower priorities will be `nil`). """ @spec from_array(BACnetArray.t(subtype)) :: t(subtype) when subtype: var def from_array(array) def from_array(%BACnetArray{size: size} = array) when size <= 16 do array |> BACnetArray.to_list() |> from_list() end def from_array(%BACnetArray{} = _array) do raise ArgumentError, "The array must have a size of maximum 16" end @doc """ Create a Priority Array from a list (or any other enumerable). The types of the list values are not checked. However, they should all be the same. The length of the list must be 16 or smaller (lower priorities will be `nil`). When using a key-value based enumerable, if the key is a priority number, then it will be used as such. """ @spec from_list(Enumerable.t(subtype)) :: t(subtype) when subtype: var def from_list(list) do Enumerable.impl_for!(list) # Force "manual" traversal for lists - as for lists, the whole list will always be traversed! unless (is_list(list) and Enum.count_until(list, fn _val -> true end, 17) <= 16) or (not is_list(list) and Enum.count_until(list, 17) <= 16) do raise ArgumentError, "The list must have a length of maximum 16" end {prioarr, _pos} = Enum.reduce(list, {%{}, 1}, fn {key, item}, {acc, prio} when key in 1..16 -> {Map.put(acc, int_to_atom(key), item), prio} item, {acc, prio} -> {Map.put(acc, int_to_atom(prio), item), prio + 1} end) struct(__MODULE__, prioarr) end @doc """ Gets the value from `key` and updates it, all in one pass. `key` is the priority number or the struct field name. `fun` is called with the current value under `key` in `array` and must return a two-element tuple: the current value (the retrieved value, which can be operated on before being returned) and the new value to be stored under `key` in the resulting new array. `fun` may also return `:pop`, which means the current value will be set to `nil`. This function is implemented for the `Access` behaviour. """ @spec get_and_update( t(subtype), 1..16 | atom(), (subtype | nil -> {current_value :: subtype | nil, new_value :: subtype | nil} | :pop) ) :: {current_value :: subtype | nil, new_struct :: t(subtype)} when subtype: var def get_and_update(%__MODULE__{} = array, key, fun) when (key in 1..16//1 or key in @fields) and is_function(fun, 1) do actual_key = key_to_struct_key(key) {:ok, current} = fetch(array, actual_key) case fun.(current) do {get, update} -> {get, %{array | actual_key => update}} :pop -> {current, %{array | actual_key => nil}} other -> raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}" end end @doc """ Get the highest active priority value. If none is active, `nil` is returned. """ @spec get_value(t(subtype)) :: {priority :: 1..16, value :: subtype} | nil when subtype: var def get_value(%__MODULE__{} = array) do get_priority_value(@fields, @priorities, array) end @doc """ Resets the value associated with `key` in `array` to `nil` and returns the value and the updated array. `key` is the priority number or the struct field name. It returns `{value, updated_array}` where `value` is the value of the key and `updated_map` is the result of setting `key` to `nil`. This function is implemented for the `Access` behaviour. """ @spec pop(t(subtype), 1..16 | atom()) :: {value :: subtype | nil, updated_array :: t(subtype)} when subtype: var def pop(%__MODULE__{} = array, key) when key in 1..16//1 or key in @fields do actual_key = key_to_struct_key(key) {:ok, value} = fetch(array, key) {value, %{array | actual_key => nil}} end @doc """ Create a BACnetArray from a Priority Array. """ @spec to_array(t(subtype)) :: BACnetArray.t(subtype | nil) when subtype: var def to_array(%__MODULE__{} = array) do array |> to_list() |> BACnetArray.from_list() end @doc """ Create a list from a Priority Array. """ @spec to_list(t(subtype)) :: list(subtype | nil) when subtype: var def to_list(%__MODULE__{} = array) do array |> Map.from_struct() |> Enum.sort_by(fn {key, _val} -> atom_to_int(key) end) |> Enum.map(fn {_key, val} -> val end) end @doc """ Validates whether the given priority array is in form valid. It only validates the struct is valid as per type specification. Optionally, a type can be given to be verified, so that each priority is either nil or of that type (see `BACnet.BeamTypes.check_type/2`). """ @spec valid?(t(), BeamTypes.typechecker_types()) :: boolean() def valid?(t, type \\ :any) def valid?(%__MODULE__{} = t, type) do Enum.all?(Map.from_struct(t), fn {_key, nil} -> true {_key, val} -> BeamTypes.check_type(type, val) end) end defp get_priority_value([], _list, %__MODULE__{} = _array), do: nil defp get_priority_value([priority | tail], [prionum | tail2], %__MODULE__{} = array) do case Map.get(array, priority) do nil -> get_priority_value(tail, tail2, array) value -> {prionum, value} end end @spec key_to_struct_key(1..16 | atom()) :: atom() defp key_to_struct_key(key) when key in 1..16, do: int_to_atom(key) defp key_to_struct_key(key), do: key @doc false @spec int_to_atom(1..16) :: atom() def int_to_atom(1), do: :priority_1 def int_to_atom(2), do: :priority_2 def int_to_atom(3), do: :priority_3 def int_to_atom(4), do: :priority_4 def int_to_atom(5), do: :priority_5 def int_to_atom(6), do: :priority_6 def int_to_atom(7), do: :priority_7 def int_to_atom(8), do: :priority_8 def int_to_atom(9), do: :priority_9 def int_to_atom(10), do: :priority_10 def int_to_atom(11), do: :priority_11 def int_to_atom(12), do: :priority_12 def int_to_atom(13), do: :priority_13 def int_to_atom(14), do: :priority_14 def int_to_atom(15), do: :priority_15 def int_to_atom(16), do: :priority_16 @doc false @spec atom_to_int(atom()) :: 1..16 def atom_to_int(:priority_1), do: 1 def atom_to_int(:priority_2), do: 2 def atom_to_int(:priority_3), do: 3 def atom_to_int(:priority_4), do: 4 def atom_to_int(:priority_5), do: 5 def atom_to_int(:priority_6), do: 6 def atom_to_int(:priority_7), do: 7 def atom_to_int(:priority_8), do: 8 def atom_to_int(:priority_9), do: 9 def atom_to_int(:priority_10), do: 10 def atom_to_int(:priority_11), do: 11 def atom_to_int(:priority_12), do: 12 def atom_to_int(:priority_13), do: 13 def atom_to_int(:priority_14), do: 14 def atom_to_int(:priority_15), do: 15 def atom_to_int(:priority_16), do: 16 end