Skip to content

DynamicGroupController

Attention

DynamicGroupController is new and still viewed as experimental. Its configuration, operation and documentation are subject to change, and probably particular at risk to do so based on feedback from users early on.

DynamicGroupController lets you create groups of entities. The inclusion of the term dynamic in its name reflects its ability to change the group membership in response to its members ability to meet criteria that may include specific attribute values or ranges. For example, you can create a group for entities that are battery powered and have battery level below a certain threshold. Any time an entity's battery level is updated, the group will automatically determine if that entity should have membership or not.

Using that example, let's do a sample configuration. First, like any controller, it needs to have an entry in the controllers section of your reactor.yaml file:

controllers:
  - id: groups
    name: Dynamic Group Controller
    enable: true
    implementation: DynamicGroupController
    config:
      groups:

From here, we'll add the specific entries we need to create a group called low_battery_entities:

    config:
      groups:
        "low_battery_entities":
          name: Low Battery Entities
          select:
            - include_capability:
              - battery_power
          filter_expression: "entity.attributes.battery_power.level < 0.35"

The first line under groups: establishes the ID of the group; each group must have a unique ID (groups are themselves entities, so this rule is consistent with all other entities).

The select section tells DynamicGroupController what entities from all of those available in the system it should choose as eligible for the group. In this example, we are chosing only those entities that have the battery_power capability.

The possible selectors are:

  • include_controller — the value can be a string or an array of strings; entities from any of the given controller IDs will be brought into the selection set;
  • exclude_controller — the value can be a string or an array of strings; as each entity is checked for eligibility, if it comes from any of the listed controllers, it is rejected (removed from the set/group);
  • include_group — the value can be a string or array of strings, which are canonical IDs or names (if unique) of another group; each specified group's members are included in the result set.
  • exclude_group — the value can be a string or array of strings, which are canonical IDs or names (if unique) of another group; each specified group's members are removed from the result set.
  • include_entity — the value can be a string or array of strings, which are canonical IDs or names (if unique); each specified entity is added to the result set; if any element is a regular expression bounded by / characters (e.g. "/^hass>input_boolean/"), all entities with canonical IDs matching the pattern will be included;
  • exclude_entity — the value can be a string or array of strings, which are canonical IDs or names (if unique); each specified entity is removed from the result set; if any element is a regular expression bounded by / characters (e.g. "/^hass>input_boolean/"), all entities with canonical IDs matching the pattern will be excluded;
  • include_capability — the value can be a string or array of strings, which are capability names; if an entity of the set has any of the listed capabilities; it remains in the set, otherwise it is removed;
  • exclude_capability — the value can be a string or array of strings, which are capability names; if an entity of the set has any of the listed capabilities, it is removed from the set; otherwise it remains.

Notice that select is an array (the - starting the selector line means it's an array element). You can have any number of selectors, and they are interpreted in order, with each selector modifying the set of entities resulting from its predecessor. You are required to have at least one selector in the select section.

If the first selector processed is one of the include_ selectors, then the set is assumed to be empty at the start, and after that first selector completes, the set will contain only those entities matching the selector. If the first selector is one of the exclude_ selectors, then the starting set is assumed to include all entities in the system, and the exclusion is applied to that set. For example, the lone selector exclude_capability: battery_power would result in a set of all of the entities in the system that are not battery powered.

To embelish our example just a bit, for illustration, let's say we have a device with problematic battery reporting, so we want to exclude that from this group (perhaps we'll handle it separately with other rules). If the device's entity has canonical ID vera>device_1789 here's how we exclude it:

          select:
            - include_capability: battery_power
            - exclude_entity: 'vera>device_1789'

A little bit of logic is possible based on the structure of the selectors you give. For example, if you give this as your select list:

          select:
            - include_capability:
              - temperature_sensor
              - humidity_sensor

...then the list of eligible entities will be all those that are either temperature sensors or humidity sensors (or both); when we give an array of capabilities to include_capability, matching any of them accepts the entity. In contrast, if we restructure our selectors like this:

          select:
            - include_capability: temperature_sensor
            - include_capability: humidity_sensor

...then the list of eligible entities will be only those that are both temperature sensors and humidity sensors (combined). Here, the first selector reduces the set to only those entities with the temperature_sensor capability, and the second selector further reduces that result set to only those that also have the humidity_sensor capability. So in effect, listing them together in one selector is an "OR", and listing them separately makes an "AND".

Filtering - The Dynamic Part

Once the set of eligble entities is established, a filter can be used to further refine the members of the set, and this is where the real dynamic aspect of the set is realized. The filter_expression (shown above) will be run against each eligible entity to determine its membership status in the group; if the expression returns (boolean) true, then the entity will be a member of the group; otherwise, it will not. Any time an eligible entity changes, it is re-evaluated for membership, and the entity will be removed from or added to the group as the result requires.

Attention

The filter expression is not allowed to use getEntity(), matchEntities(), or performAction(); these are not defined for the filter expression evaluation and will throw an undefined function error if attempted. Global variables are also not accessible from within these expressions.

If your expression gets long, consider using the YAML block scalar style:

          filter_expression: >
            very long expression can go here and can
            even span several
            lines if you wish

Assigning and Determining the Primary Attribute

All entities in Reactor can have a primary attribute assigned, and groups are no exception. By default, a group's primary attribute is sys_group.empty, a boolean value that is true when the group is empty, and false if it has any members. You can override the primary attribute for a group and provide an expression to determine its value, if you wish.

To override the default primary attribute, you need to add two configuration keys: primary_attribute and primary_attribute_value. The former sets the attribute to be designated as primary, and must be in capabilityName.attributeName form (e.g. binary_sensor.state). The latter is an expression, the value of which will be used as the (primary) attribute value.

Here's an example that we might use to set the primary attribute to binary_sensor.state, and set its value true when any light in the group is on (it will be false if all lights in the group are off):

          primary_attribute: "binary_sensor.state"
          primary_attribute_value: |
            d = false;
            each id in members: d = getEntity(id)?.attributes?.power_switch?.state or d,
            d

There are two things to notice in the value expression. First, it's a multi-statement expression that uses a local variable (d, defined on the first line) to "accumulate" the value of the state as it iterates over all of the entities in group (second line), and finally "returns" that value (third line). Second, it uses the predefined variable members, which in this context will be an array of the entity IDs of the group's members. The end result of this expression is that it will return false unless any member entity is powered on.

Here's a slightly modified example that will be true only if all group members are on:

          primary_attribute: "binary_sensor.state"
          primary_attribute_value: |
            d = true;
            each id in members: d = bool(getEntity(id)?.attributes?.power_switch?.state) and d,
            d

In this example, we've wrapped the attribute check in the bool() function, because it's possible for the attribute value to be null (e.g. if a group member doesn't provide power_switch or the device is offline with some hubs). The result of null and true is null, so we need to guard for that and make sure we only produce a true or false result. Using bool() will coerce a null attribute value to false and keep consistent boolean values in the expression execution.

Defining Custom Attributes

If you can't find a system-defined capability that has an appropriate attribute for your purpose, you can always define a custom attribute. By convention, the capability for user-defined custom attributes for groups would be x_group_user. If you use this capability, you'll avoid any collisions with future changes in the system-defined capabilities and attributes. Append your attribute name to it. For example, if you need an attribute to tell you when all the group's lights were in color mode showing blue, you might use x_group_user.all_lights_blue.

Group Actions

When one creates a homogenous group (i.e. a group with the same or similar types of devices, like switches and dimmers), it is sometimes desirable to be able to perform an action on all members of the group. To enable this for a group, set the group_actions configuration switch to true in the group's configuration (at the same level as select).

          select:
            - include_capability:
              - power_switch
              - dimming
          group_actions: true

This will cause DynamicGroupController to examine all of the entities selected for group membership (before filtering by filter_expression) and determine the set of actions possible for any group member. All of these capabilities and their actions will be extended to the group. This can be a very long list, which is why this feature is not enabled by default.

When performing actions on a group, any member entity that is not capable of performing the selected action will simply be ignored. For example, if you set the dimming level of a group, but one member is just a binary switch, no action will be taken on it.

Using Dynamic Groups

It seems likely that the two most common ways of using dynamic groups will be:

  1. In expressions; dynamic groups can be referred to in the groups key of the matchEntities() function, or by passing the group's canonical ID to getEntity() and accessing the membership list stored in its attributes. The former is probably preferable, as it's a little easier to read and understand what is going on. As an example, here are expressions that use each of the foregoing methods to turn the list of low-battery devices into a list of entity names (rather than entity IDs):

    each id in matchEntities({group:'groups>low_battery_entities'}): getEntity( id ).name
    each id in getEntity('groups>low_battery_entities').attributes.sys_group.members: getEntity( id ).name
    
  2. In Entity Attribute conditions, you can use the changes operator on the group's sys_group.members attribute to trigger when the membership list changes, or use the in operator to see if an important entity is in the array, etc.

Dynamic Groups are Efficient

Dynamic groups are much more efficient than filtering entities in expressions using matchEntities() with various include/exclude options, and the filtering offered by DynamicGroupController is much more extensive and flexible. You should lean towards using dynamic groups as much as possible, and minimize the use of matchEntities() to just retrieving group members and handling them... leave the hard work for DynamicGroupController.

Updated: 2024-May-07