Group sensor calculate attributes (#106972)

* Group sensor calculate attributes

* Use entity helpers

* Fix sensor tests

* Test change of uom

* Add tests and fix UoM issue

* Fix test

* Fix state class

* repair and logs

* delete issues

* pass through hass

* Update descriotion text to be more descriptive

* Comments

* Add pr to comment

* fix if in updating

* Fix test valid units

* Fix strings

* Fix issues
This commit is contained in:
G Johansson 2024-01-23 08:16:51 +01:00 committed by GitHub
parent 329eca4918
commit f9a4840ce2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 416 additions and 95 deletions

View File

@ -89,7 +89,7 @@ async def async_setup_entry(
@callback
def async_create_preview_binary_sensor(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> BinarySensorGroup:
"""Create a preview sensor."""
return BinarySensorGroup(

View File

@ -269,7 +269,7 @@ PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {}
CREATE_PREVIEW_ENTITY: dict[
str,
Callable[[str, dict[str, Any]], GroupEntity | MediaPlayerGroup],
Callable[[HomeAssistant, str, dict[str, Any]], GroupEntity | MediaPlayerGroup],
] = {
"binary_sensor": async_create_preview_binary_sensor,
"cover": async_create_preview_cover,
@ -392,7 +392,9 @@ def ws_start_preview(
)
)
preview_entity = CREATE_PREVIEW_ENTITY[group_type](name, validated)
preview_entity: GroupEntity | MediaPlayerGroup = CREATE_PREVIEW_ENTITY[group_type](
hass, name, validated
)
preview_entity.hass = hass
preview_entity.registry_entry = entity_registry_entry

View File

@ -97,7 +97,7 @@ async def async_setup_entry(
@callback
def async_create_preview_cover(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> CoverGroup:
"""Create a preview sensor."""
return CoverGroup(

View File

@ -90,7 +90,7 @@ async def async_setup_entry(
@callback
def async_create_preview_event(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> EventGroup:
"""Create a preview sensor."""
return EventGroup(

View File

@ -91,7 +91,9 @@ async def async_setup_entry(
@callback
def async_create_preview_fan(name: str, validated_config: dict[str, Any]) -> FanGroup:
def async_create_preview_fan(
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> FanGroup:
"""Create a preview sensor."""
return FanGroup(
None,

View File

@ -112,7 +112,7 @@ async def async_setup_entry(
@callback
def async_create_preview_light(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> LightGroup:
"""Create a preview sensor."""
return LightGroup(

View File

@ -91,7 +91,9 @@ async def async_setup_entry(
@callback
def async_create_preview_lock(name: str, validated_config: dict[str, Any]) -> LockGroup:
def async_create_preview_lock(
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> LockGroup:
"""Create a preview sensor."""
return LockGroup(
None,

View File

@ -109,7 +109,7 @@ async def async_setup_entry(
@callback
def async_create_preview_media_player(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> MediaPlayerGroup:
"""Create a preview sensor."""
return MediaPlayerGroup(

View File

@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
DOMAIN,
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
STATE_CLASSES_SCHEMA,
UNIT_CONVERTERS,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
@ -34,11 +35,22 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import (
get_capability,
get_device_class,
get_unit_of_measurement,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from . import GroupEntity
from . import DOMAIN as GROUP_DOMAIN, GroupEntity
from .const import CONF_IGNORE_NON_NUMERIC
DEFAULT_NAME = "Sensor Group"
@ -97,6 +109,7 @@ async def async_setup_platform(
async_add_entities(
[
SensorGroup(
hass,
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
@ -123,6 +136,7 @@ async def async_setup_entry(
async_add_entities(
[
SensorGroup(
hass,
config_entry.entry_id,
config_entry.title,
entities,
@ -138,10 +152,11 @@ async def async_setup_entry(
@callback
def async_create_preview_sensor(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> SensorGroup:
"""Create a preview sensor."""
return SensorGroup(
hass,
None,
name,
validated_config[CONF_ENTITIES],
@ -280,6 +295,7 @@ class SensorGroup(GroupEntity, SensorEntity):
def __init__(
self,
hass: HomeAssistant,
unique_id: str | None,
name: str,
entity_ids: list[str],
@ -290,14 +306,13 @@ class SensorGroup(GroupEntity, SensorEntity):
device_class: SensorDeviceClass | None,
) -> None:
"""Initialize a sensor group."""
self.hass = hass
self._entity_ids = entity_ids
self._sensor_type = sensor_type
self._attr_state_class = state_class
self.calc_state_class: SensorStateClass | None = None
self._attr_device_class = device_class
self.calc_device_class: SensorDeviceClass | None = None
self._attr_native_unit_of_measurement = unit_of_measurement
self.calc_unit_of_measurement: str | None = None
self._state_class = state_class
self._device_class = device_class
self._native_unit_of_measurement = unit_of_measurement
self._valid_units: set[str | None] = set()
self._attr_name = name
if name == DEFAULT_NAME:
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
@ -311,6 +326,16 @@ class SensorGroup(GroupEntity, SensorEntity):
self._state_incorrect: set[str] = set()
self._extra_state_attribute: dict[str, Any] = {}
async def async_added_to_hass(self) -> None:
"""When added to hass."""
self._attr_state_class = self._calculate_state_class(self._state_class)
self._attr_device_class = self._calculate_device_class(self._device_class)
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
self._native_unit_of_measurement
)
self._valid_units = self._get_valid_units()
await super().async_added_to_hass()
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the sensor group state."""
@ -321,7 +346,16 @@ class SensorGroup(GroupEntity, SensorEntity):
if (state := self.hass.states.get(entity_id)) is not None:
states.append(state.state)
try:
sensor_values.append((entity_id, float(state.state), state))
numeric_state = float(state.state)
if (
self._valid_units
and (uom := state.attributes["unit_of_measurement"])
in self._valid_units
):
numeric_state = UNIT_CONVERTERS[self.device_class].convert(
numeric_state, uom, self.native_unit_of_measurement
)
sensor_values.append((entity_id, numeric_state, state))
if entity_id in self._state_incorrect:
self._state_incorrect.remove(entity_id)
except ValueError:
@ -330,9 +364,29 @@ class SensorGroup(GroupEntity, SensorEntity):
self._state_incorrect.add(entity_id)
_LOGGER.warning(
"Unable to use state. Only numerical states are supported,"
" entity %s with value %s excluded from calculation",
" entity %s with value %s excluded from calculation in %s",
entity_id,
state.state,
self.entity_id,
)
continue
except (KeyError, HomeAssistantError):
# This exception handling can be simplified
# once sensor entity doesn't allow incorrect unit of measurement
# with a device class, implementation see PR #107639
valid_states.append(False)
if entity_id not in self._state_incorrect:
self._state_incorrect.add(entity_id)
_LOGGER.warning(
"Unable to use state. Only entities with correct unit of measurement"
" is supported when having a device class,"
" entity %s, value %s with device class %s"
" and unit of measurement %s excluded from calculation in %s",
entity_id,
state.state,
self.device_class,
state.attributes.get("unit_of_measurement"),
self.entity_id,
)
continue
valid_states.append(True)
@ -350,7 +404,6 @@ class SensorGroup(GroupEntity, SensorEntity):
return
# Calculate values
self._calculate_entity_properties()
self._extra_state_attribute, self._attr_native_value = self._state_calc(
sensor_values
)
@ -360,13 +413,6 @@ class SensorGroup(GroupEntity, SensorEntity):
"""Return the state attributes of the sensor."""
return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute}
@property
def device_class(self) -> SensorDeviceClass | None:
"""Return device class."""
if self._attr_device_class is not None:
return self._attr_device_class
return self.calc_device_class
@property
def icon(self) -> str | None:
"""Return the icon.
@ -377,59 +423,165 @@ class SensorGroup(GroupEntity, SensorEntity):
return "mdi:calculator"
return None
@property
def state_class(self) -> SensorStateClass | str | None:
"""Return state class."""
if self._attr_state_class is not None:
return self._attr_state_class
return self.calc_state_class
@property
def native_unit_of_measurement(self) -> str | None:
"""Return native unit of measurement."""
if self._attr_native_unit_of_measurement is not None:
return self._attr_native_unit_of_measurement
return self.calc_unit_of_measurement
def _calculate_entity_properties(self) -> None:
"""Calculate device_class, state_class and unit of measurement."""
device_classes = []
state_classes = []
unit_of_measurements = []
if (
self._attr_device_class
and self._attr_state_class
and self._attr_native_unit_of_measurement
):
return
def _calculate_state_class(
self, state_class: SensorStateClass | None
) -> SensorStateClass | None:
"""Calculate state class.
If user has configured a state class we will use that.
If a state class is not set then test if same state class
on source entities and use that.
Otherwise return no state class.
"""
if state_class:
return state_class
state_classes: list[SensorStateClass] = []
for entity_id in self._entity_ids:
if (state := self.hass.states.get(entity_id)) is not None:
device_classes.append(state.attributes.get("device_class"))
state_classes.append(state.attributes.get("state_class"))
unit_of_measurements.append(state.attributes.get("unit_of_measurement"))
try:
_state_class = get_capability(self.hass, entity_id, "state_class")
except HomeAssistantError:
return None
if not _state_class:
return None
state_classes.append(_state_class)
self.calc_device_class = None
self.calc_state_class = None
self.calc_unit_of_measurement = None
if all(x == state_classes[0] for x in state_classes):
async_delete_issue(
self.hass, DOMAIN, f"{self.entity_id}_state_classes_not_matching"
)
return state_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
f"{self.entity_id}_state_classes_not_matching",
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="state_classes_not_matching",
translation_placeholders={
"entity_id": self.entity_id,
"source_entities": ", ".join(self._entity_ids),
"state_classes:": ", ".join(state_classes),
},
)
return None
# Calculate properties and save if all same
if (
not self._attr_device_class
and device_classes
and all(x == device_classes[0] for x in device_classes)
def _calculate_device_class(
self, device_class: SensorDeviceClass | None
) -> SensorDeviceClass | None:
"""Calculate device class.
If user has configured a device class we will use that.
If a device class is not set then test if same device class
on source entities and use that.
Otherwise return no device class.
"""
if device_class:
return device_class
device_classes: list[SensorDeviceClass] = []
for entity_id in self._entity_ids:
try:
_device_class = get_device_class(self.hass, entity_id)
except HomeAssistantError:
return None
if not _device_class:
return None
device_classes.append(SensorDeviceClass(_device_class))
if all(x == device_classes[0] for x in device_classes):
async_delete_issue(
self.hass, DOMAIN, f"{self.entity_id}_device_classes_not_matching"
)
return device_classes[0]
async_create_issue(
self.hass,
GROUP_DOMAIN,
f"{self.entity_id}_device_classes_not_matching",
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="device_classes_not_matching",
translation_placeholders={
"entity_id": self.entity_id,
"source_entities": ", ".join(self._entity_ids),
"device_classes:": ", ".join(device_classes),
},
)
return None
def _calculate_unit_of_measurement(
self, unit_of_measurement: str | None
) -> str | None:
"""Calculate the unit of measurement.
If user has configured a unit of measurement we will use that.
If a device class is set then test if unit of measurements are compatible.
If no device class or uom's not compatible we will use no unit of measurement.
"""
if unit_of_measurement:
return unit_of_measurement
unit_of_measurements: list[str] = []
for entity_id in self._entity_ids:
try:
_unit_of_measurement = get_unit_of_measurement(self.hass, entity_id)
except HomeAssistantError:
return None
if not _unit_of_measurement:
return None
unit_of_measurements.append(_unit_of_measurement)
# Ensure only valid unit of measurements for the specific device class can be used
if (device_class := self.device_class) in UNIT_CONVERTERS and all(
x in UNIT_CONVERTERS[device_class].VALID_UNITS for x in unit_of_measurements
):
self.calc_device_class = device_classes[0]
async_delete_issue(
self.hass, DOMAIN, f"{self.entity_id}_uoms_not_matching_device_class"
)
async_delete_issue(
self.hass, DOMAIN, f"{self.entity_id}_uoms_not_matching_no_device_class"
)
return unit_of_measurements[0]
if device_class:
async_create_issue(
self.hass,
GROUP_DOMAIN,
f"{self.entity_id}_uoms_not_matching_device_class",
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="uoms_not_matching_device_class",
translation_placeholders={
"entity_id": self.entity_id,
"device_class": device_class,
"source_entities": ", ".join(self._entity_ids),
"uoms:": ", ".join(unit_of_measurements),
},
)
else:
async_create_issue(
self.hass,
GROUP_DOMAIN,
f"{self.entity_id}_uoms_not_matching_no_device_class",
is_fixable=False,
is_persistent=False,
severity=IssueSeverity.WARNING,
translation_key="uoms_not_matching_no_device_class",
translation_placeholders={
"entity_id": self.entity_id,
"source_entities": ", ".join(self._entity_ids),
"uoms:": ", ".join(unit_of_measurements),
},
)
return None
def _get_valid_units(self) -> set[str | None]:
"""Return valid units.
If device class is set and compatible unit of measurements.
"""
if (
not self._attr_state_class
and state_classes
and all(x == state_classes[0] for x in state_classes)
):
self.calc_state_class = state_classes[0]
if (
not self._attr_unit_of_measurement
and unit_of_measurements
and all(x == unit_of_measurements[0] for x in unit_of_measurements)
):
self.calc_unit_of_measurement = unit_of_measurements[0]
device_class := self.device_class
) in UNIT_CONVERTERS and self.native_unit_of_measurement:
return UNIT_CONVERTERS[device_class].VALID_UNITS
return set()

View File

@ -249,5 +249,23 @@
}
}
}
},
"issues": {
"uoms_not_matching_device_class": {
"title": "Unit of measurements are not correct",
"description": "Unit of measurements `{uoms}` of input sensors `{source_entities}` are not compatible and can't be converted with the device class `{device_class}` of sensor group `{entity_id}`.\n\nPlease correct the unit of measurements on the source entities and reload the group sensor to fix this issue."
},
"uoms_not_matching_no_device_class": {
"title": "Unit of measurements is not correct",
"description": "Unit of measurements `{uoms}` of input sensors `{source_entities}` are not compatible using no device class of sensor group `{entity_id}`.\n\nPlease correct the unit of measurements on the source entities or set a proper device class on the sensor group and reload the group sensor to fix this issue."
},
"device_classes_not_matching": {
"title": "Device classes is not correct",
"description": "Device classes `{device_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the device classes on the source entities and reload the group sensor to fix this issue."
},
"state_classes_not_matching": {
"title": "State classes is not correct",
"description": "Device classes `{state_classes}` on source entities `{source_entities}` needs to be same for sensor group `{entity_id}`.\n\nPlease correct the state classes on the source entities and reload the group sensor to fix this issue."
}
}
}

View File

@ -87,7 +87,7 @@ async def async_setup_entry(
@callback
def async_create_preview_switch(
name: str, validated_config: dict[str, Any]
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> SwitchGroup:
"""Create a preview sensor."""
return SwitchGroup(

View File

@ -32,6 +32,7 @@ from homeassistant.const import (
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
import homeassistant.helpers.entity_registry as er
from homeassistant.setup import async_setup_component
@ -62,7 +63,7 @@ PRODUCT_VALUE = prod(VALUES)
("product", PRODUCT_VALUE, {}),
],
)
async def test_sensors(
async def test_sensors2(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
sensor_type: str,
@ -88,7 +89,7 @@ async def test_sensors(
value,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME,
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
ATTR_UNIT_OF_MEASUREMENT: "L",
},
)
@ -105,7 +106,7 @@ async def test_sensors(
assert state.attributes.get(key) == value
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L"
entity = entity_registry.async_get(f"sensor.sensor_group_{sensor_type}")
@ -146,7 +147,8 @@ async def test_sensors_attributes_defined(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.sensor_group_sum")
assert state.state == str(float(SUM_VALUE))
# Liter to M3 = 1:0.001
assert state.state == str(float(SUM_VALUE * 0.001))
assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING
@ -324,9 +326,6 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
}
}
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
entity_ids = config["sensor"]["entities"]
hass.states.async_set(
@ -334,7 +333,7 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
VALUES[0],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.MEASUREMENT,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
@ -343,35 +342,181 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
VALUES[1],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.MEASUREMENT,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
hass.states.async_set(
entity_ids[2],
VALUES[2],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "Wh",
},
)
await hass.async_block_till_done()
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sum")
assert state.state == str(float(sum([VALUES[0], VALUES[1], VALUES[2] / 1000])))
assert state.attributes.get("device_class") == "energy"
assert state.attributes.get("state_class") == "total"
assert state.attributes.get("unit_of_measurement") == "kWh"
# Test that a change of source entity's unit of measurement
# is converted correctly by the group sensor
hass.states.async_set(
entity_ids[2],
VALUES[2],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sum")
assert state.state == str(float(sum([VALUES[0], VALUES[1]])))
assert state.attributes.get("device_class") == "energy"
assert state.attributes.get("state_class") == "measurement"
assert state.attributes.get("unit_of_measurement") == "kWh"
assert state.state == str(float(sum(VALUES)))
async def test_sensor_calculated_properties_not_same(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test the sensor calculating device_class, state_class and unit of measurement not same."""
config = {
SENSOR_DOMAIN: {
"platform": GROUP_DOMAIN,
"name": "test_sum",
"type": "sum",
"entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
"unique_id": "very_unique_id_sum_sensor",
}
}
entity_ids = config["sensor"]["entities"]
hass.states.async_set(
entity_ids[0],
VALUES[0],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
hass.states.async_set(
entity_ids[1],
VALUES[1],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
hass.states.async_set(
entity_ids[2],
VALUES[2],
{
"device_class": SensorDeviceClass.BATTERY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": None,
"device_class": SensorDeviceClass.CURRENT,
"state_class": SensorStateClass.MEASUREMENT,
"unit_of_measurement": "A",
},
)
await hass.async_block_till_done()
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sum")
assert state.state == str(sum(VALUES))
assert state.state == str(float(sum(VALUES)))
assert state.attributes.get("device_class") is None
assert state.attributes.get("state_class") is None
assert state.attributes.get("unit_of_measurement") is None
assert issue_registry.async_get_issue(
GROUP_DOMAIN, "sensor.test_sum_uoms_not_matching_no_device_class"
)
assert issue_registry.async_get_issue(
GROUP_DOMAIN, "sensor.test_sum_device_classes_not_matching"
)
assert issue_registry.async_get_issue(
GROUP_DOMAIN, "sensor.test_sum_state_classes_not_matching"
)
async def test_sensor_calculated_result_fails_on_uom(hass: HomeAssistant) -> None:
"""Test the sensor calculating fails as UoM not part of device class."""
config = {
SENSOR_DOMAIN: {
"platform": GROUP_DOMAIN,
"name": "test_sum",
"type": "sum",
"entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
"unique_id": "very_unique_id_sum_sensor",
}
}
entity_ids = config["sensor"]["entities"]
hass.states.async_set(
entity_ids[0],
VALUES[0],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
hass.states.async_set(
entity_ids[1],
VALUES[1],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
hass.states.async_set(
entity_ids[2],
VALUES[2],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
"unit_of_measurement": "kWh",
},
)
await hass.async_block_till_done()
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sum")
assert state.state == str(float(sum(VALUES)))
assert state.attributes.get("device_class") == "energy"
assert state.attributes.get("state_class") == "total"
assert state.attributes.get("unit_of_measurement") == "kWh"
hass.states.async_set(
entity_ids[2],
12,
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
},
True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sum")
assert state.state == STATE_UNKNOWN
assert state.attributes.get("device_class") == "energy"
assert state.attributes.get("state_class") == "total"
assert state.attributes.get("unit_of_measurement") == "kWh"
async def test_last_sensor(hass: HomeAssistant) -> None:
"""Test the last sensor."""