diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index d1e91db8f86..d63dcb5e8f2 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -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( diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 93160b0db5b..488f5e131f3 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -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 diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index d22184c0922..78d29378076 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -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( diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index ca0c88867fe..b98991e13fc 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -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( diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 4e3bb824266..afd240c5767 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -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, diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 3c1ad7f0d57..5a113491891 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -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( diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 5558eab5475..4a6fdc3e2ed 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -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, diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index b85fbf32a0d..aa38f364d93 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -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( diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index c35c96d38aa..84827ef89fa 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -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() diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index c5cebbc4707..25ae20da995 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -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." + } } } diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index 64bc9a99636..3f68d7125aa 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -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( diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index 71a53042938..12bb8d0f7de 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -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."""