diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 9674b32df9b..2326851491c 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -1,12 +1,13 @@ """Validate the energy preferences provide valid data.""" from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Mapping, Sequence import dataclasses from typing import Any from homeassistant.components import recorder, sensor from homeassistant.const import ( + ATTR_DEVICE_CLASS, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, STATE_UNAVAILABLE, @@ -19,14 +20,16 @@ from homeassistant.core import HomeAssistant, callback, valid_entity_id from . import data from .const import DOMAIN -ENERGY_USAGE_UNITS = (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) +ENERGY_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY,) +ENERGY_USAGE_UNITS = { + sensor.DEVICE_CLASS_ENERGY: (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) +} ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy" -GAS_USAGE_UNITS = ( - ENERGY_WATT_HOUR, - ENERGY_KILO_WATT_HOUR, - VOLUME_CUBIC_METERS, - VOLUME_CUBIC_FEET, -) +GAS_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY, sensor.DEVICE_CLASS_GAS) +GAS_USAGE_UNITS = { + sensor.DEVICE_CLASS_ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.DEVICE_CLASS_GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), +} GAS_UNIT_ERROR = "entity_unexpected_unit_gas" @@ -59,7 +62,8 @@ class EnergyPreferencesValidation: def _async_validate_usage_stat( hass: HomeAssistant, stat_value: str, - allowed_units: Sequence[str], + allowed_device_classes: Sequence[str], + allowed_units: Mapping[str, Sequence[str]], unit_error: str, result: list[ValidationIssue], ) -> None: @@ -106,19 +110,29 @@ def _async_validate_usage_stat( ValidationIssue("entity_negative_state", stat_value, current_value) ) - unit = state.attributes.get("unit_of_measurement") + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + if device_class not in allowed_device_classes: + result.append( + ValidationIssue( + "entity_unexpected_device_class", + stat_value, + device_class, + ) + ) + else: + unit = state.attributes.get("unit_of_measurement") - if unit not in allowed_units: - result.append(ValidationIssue(unit_error, stat_value, unit)) + if device_class and unit not in allowed_units.get(device_class, []): + result.append(ValidationIssue(unit_error, stat_value, unit)) - state_class = state.attributes.get("state_class") + state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) - supported_state_classes = [ + allowed_state_classes = [ sensor.STATE_CLASS_MEASUREMENT, sensor.STATE_CLASS_TOTAL, sensor.STATE_CLASS_TOTAL_INCREASING, ] - if state_class not in supported_state_classes: + if state_class not in allowed_state_classes: result.append( ValidationIssue( "entity_unexpected_state_class_total_increasing", @@ -236,6 +250,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, flow["stat_energy_from"], + ENERGY_USAGE_DEVICE_CLASSES, ENERGY_USAGE_UNITS, ENERGY_UNIT_ERROR, source_result, @@ -258,6 +273,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, flow["stat_energy_to"], + ENERGY_USAGE_DEVICE_CLASSES, ENERGY_USAGE_UNITS, ENERGY_UNIT_ERROR, source_result, @@ -282,6 +298,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, source["stat_energy_from"], + GAS_USAGE_DEVICE_CLASSES, GAS_USAGE_UNITS, GAS_UNIT_ERROR, source_result, @@ -304,6 +321,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, source["stat_energy_from"], + ENERGY_USAGE_DEVICE_CLASSES, ENERGY_USAGE_UNITS, ENERGY_UNIT_ERROR, source_result, @@ -313,6 +331,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, source["stat_energy_from"], + ENERGY_USAGE_DEVICE_CLASSES, ENERGY_USAGE_UNITS, ENERGY_UNIT_ERROR, source_result, @@ -320,6 +339,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, source["stat_energy_to"], + ENERGY_USAGE_DEVICE_CLASSES, ENERGY_USAGE_UNITS, ENERGY_UNIT_ERROR, source_result, @@ -331,6 +351,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_usage_stat( hass, device["stat_consumption"], + ENERGY_USAGE_DEVICE_CLASSES, ENERGY_USAGE_UNITS, ENERGY_UNIT_ERROR, device_result, diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index e893c71d1f2..76b9201a001 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -45,7 +45,11 @@ async def test_validation(hass, mock_energy_manager): hass.states.async_set( f"sensor.{key}", "123", - {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, ) await mock_energy_manager.async_update( @@ -142,7 +146,11 @@ async def test_validation_device_consumption_entity_unexpected_unit( hass.states.async_set( "sensor.unexpected_unit", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) assert (await validate.async_validate(hass)).as_dict() == { @@ -194,7 +202,11 @@ async def test_validation_solar(hass, mock_energy_manager): hass.states.async_set( "sensor.solar_production", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) assert (await validate.async_validate(hass)).as_dict() == { @@ -227,12 +239,20 @@ async def test_validation_battery(hass, mock_energy_manager): hass.states.async_set( "sensor.battery_import", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) hass.states.async_set( "sensor.battery_export", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) assert (await validate.async_validate(hass)).as_dict() == { @@ -282,12 +302,20 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde hass.states.async_set( "sensor.grid_consumption_1", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) hass.states.async_set( "sensor.grid_production_1", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) assert (await validate.async_validate(hass)).as_dict() == { @@ -324,12 +352,20 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager): hass.states.async_set( "sensor.grid_consumption_1", "10.10", - {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, ) hass.states.async_set( "sensor.grid_production_1", "10.10", - {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, ) await mock_energy_manager.async_update( { @@ -402,7 +438,11 @@ async def test_validation_grid_price_errors( hass.states.async_set( "sensor.grid_consumption_1", "10.10", - {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, ) hass.states.async_set( "sensor.grid_price_1", @@ -454,18 +494,50 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded "stat_energy_from": "sensor.gas_consumption_2", "stat_cost": "sensor.gas_cost_2", }, + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption_3", + "stat_cost": "sensor.gas_cost_2", + }, + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption_4", + "stat_cost": "sensor.gas_cost_2", + }, ] } ) hass.states.async_set( "sensor.gas_consumption_1", "10.10", - {"unit_of_measurement": "beers", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, ) hass.states.async_set( "sensor.gas_consumption_2", "10.10", - {"unit_of_measurement": "kWh", "state_class": "total_increasing"}, + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, + ) + hass.states.async_set( + "sensor.gas_consumption_3", + "10.10", + { + "device_class": "gas", + "unit_of_measurement": "m³", + "state_class": "total_increasing", + }, + ) + hass.states.async_set( + "sensor.gas_consumption_4", + "10.10", + {"unit_of_measurement": "beers", "state_class": "total_increasing"}, ) hass.states.async_set( "sensor.gas_cost_2", @@ -488,6 +560,14 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded }, ], [], + [], + [ + { + "type": "entity_unexpected_device_class", + "identifier": "sensor.gas_consumption_4", + "value": None, + }, + ], ], "device_consumption": [], }