I have an automation that checks the battery level for devices and sensors and updates groups based on these checks. The full automation is below:
automation:
- alias: Battery Groups
id: battery_groups
description: ""
trigger:
- platform: time_pattern
minutes: '*'
condition: []
action:
# Doesn't add devices with float-based battery_level
- service: group.set
data:
object_id: battery_low_devices
entities: >-
{% set threshold = states('input_number.battery_threshold') | int(0) %}
{% set sensors = states.sensor | selectattr('attributes.device_class', 'defined') | selectattr('attributes.device_class', '==', 'battery') | rejectattr('state', 'in', ['unavailable', 'unknown']) | list %}
{% set device_ents = states.device_tracker | map(attribute='entity_id') | map('device_id') | map('device_entities') | sum(start=[]) | list %}
{% set low = sensors | map(attribute='state') | map('int') | select('<=', threshold) | map('string') | list %}
{{ sensors | selectattr('state', 'in', low) | selectattr('entity_id', 'in', device_ents) | sort(attribute='name') | map(attribute='entity_id') | list }}
# Doesn't add devices with float-based battery_level
- service: group.set
data:
object_id: battery_low_sensors
entities: >-
{% set threshold = states('input_number.battery_threshold') | int(0) %}
{% set sensors = states.sensor | selectattr('attributes.device_class', 'defined') | selectattr('attributes.device_class', '==', 'battery') | rejectattr('state', 'in', ['unavailable', 'unknown']) | list %}
{% set device_ents = states.device_tracker | map(attribute='entity_id') | map('device_id') | map('device_entities') | sum(start=[]) | list %}
{% set low = sensors | map(attribute='state') | map('int') | select('<=', threshold) | map('string') | list %}
{{ sensors | selectattr('state', 'in', low) | rejectattr('entity_id', 'in', device_ents) | sort(attribute='name') | map(attribute='entity_id') | list }}
# Works as is
- service: group.set
data:
object_id: battery_unavailable
entities: >-
{{ states.sensor | selectattr('attributes.device_class', 'defined') | selectattr('attributes.device_class', '==', 'battery') | selectattr('state', '==', 'unavailable') | map(attribute='entity_id') | list }}
# Works as is
- service: group.set
data:
object_id: battery_unknown
entities: >-
{{ states.sensor | selectattr('attributes.device_class', 'defined') | selectattr('attributes.device_class', '==', 'battery') | selectattr('state', '==', 'unknown') | map(attribute='entity_id') | list }}
For some background as to why Iām using a list and not just building a comma-separated value (which the group.set
service can take), itās because thereās usually not any devices with low batteries. The problem with building a comma-separated list is that if the template doesnāt find any devices, the template evaluates to an empty string, passing None
to the service call, which the service call wonāt accept. In doing so, it breaks the automation.
That said, this mostly works. The problem is with my logic - the low
list has to be converted to a type (int or float) that is comparable with the integer threshold
, and later the states
state attributes get mapped to either an int or float and checked if they exist in the low
list. If so, they populate the list of devices to get stuffed into the corresponding group. the groups end up getting filled with only the devices that have an integer representation for their battery level. This is problematic because some devices (like Aqara Zigbee thermometers) report battery level as an int while others (like First Alert Z-Wave Smoke/CO2 alarms) report battery level as a float, so the low
list is either a list of ints or floats, but checking the list with sensors | selectattr('state', 'in', low) | ... | list
fails because floats donāt exist in a list of ints and ints donāt exist in a list of floats.
Iāve tried playing around with the affected service calls (the first two in the above automation) to no avail. Iāve also tried something like this:
action:
# Fails if entities: is empty (None); also stops execution of further service calls
- service: group.set
data:
object_id: battery_low_devices
entities: >-
{% set threshold = states('input_number.battery_threshold') | int(0) %}
{% set device_ents = states.device_tracker | map(attribute='entity_id') | map('device_id') | map('device_entities') | sum(start=[]) | list %}
{% for state in states.sensor if state.state not in ['unavailable', 'unknown'] and state.entity_id in device_ents and is_state_attr(state.entity_id, 'device_class', 'battery') and state.state | int(0) <= threshold %}
{{ state.entity_id }}{% if not loop.last %}, {% endif %}
{% endfor %}
# Fails if entities: is empty (None); also stops execution of further service calls
- service: group.set
data:
object_id: battery_low_sensors
entities: >-
{%- set threshold = states('input_number.battery_threshold') | int(0) -%}
{%- set device_ents = states.device_tracker | map(attribute='entity_id') | map('device_id') | map('device_entities') | sum(start=[]) | list -%}
{%- for state in states.sensor if state.state not in ['unavailable', 'unknown'] and state.entity_id not in device_ents and is_state_attr(state.entity_id, 'device_class', 'battery') and state.state | int(0) <= threshold -%}
{{ state.entity_id }}{% if not loop.last %}, {% endif %}
{%- endfor -%}
This presents the issue I described above - when any of the templates return nothing, the service is called and gives an error āFailed to call service group.set. Entity ID is an invalid entity ID for dictionary value @ data['entities']. Got None
ā. This is why I am going the list route, which works (for example, calling group.set
with entities: []
works without issue and clears the group).
I really just need a way to store a list of ints or floats in a variable called low
and later call sensors | selectattr('state', 'in', low) | ... | list
, sowehow converting the state attrs to ints or floats while comparing them. I just canāt figure out how to do this.
Ā
Any help would be great.