From ec337101ddfb738eb3b68d82180ddf48cf72a027 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Sep 2021 20:53:43 -0700 Subject: [PATCH] Fix gas validation (#55886) --- homeassistant/components/energy/validate.py | 83 +++++++++++++++++---- tests/components/energy/test_validate.py | 56 ++++++++++++++ 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 01709081d68..9ee6df30b5e 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -1,6 +1,7 @@ """Validate the energy preferences provide valid data.""" from __future__ import annotations +from collections.abc import Sequence import dataclasses from typing import Any @@ -10,12 +11,24 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, STATE_UNAVAILABLE, STATE_UNKNOWN, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, ) 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_UNIT_ERROR = "entity_unexpected_unit_energy" +GAS_USAGE_UNITS = ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + VOLUME_CUBIC_METERS, + VOLUME_CUBIC_FEET, +) +GAS_UNIT_ERROR = "entity_unexpected_unit_gas" + @dataclasses.dataclass class ValidationIssue: @@ -43,8 +56,12 @@ class EnergyPreferencesValidation: @callback -def _async_validate_energy_stat( - hass: HomeAssistant, stat_value: str, result: list[ValidationIssue] +def _async_validate_usage_stat( + hass: HomeAssistant, + stat_value: str, + allowed_units: Sequence[str], + unit_error: str, + result: list[ValidationIssue], ) -> None: """Validate a statistic.""" has_entity_source = valid_entity_id(stat_value) @@ -91,10 +108,8 @@ def _async_validate_energy_stat( unit = state.attributes.get("unit_of_measurement") - if unit not in (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR): - result.append( - ValidationIssue("entity_unexpected_unit_energy", stat_value, unit) - ) + if unit not in allowed_units: + result.append(ValidationIssue(unit_error, stat_value, unit)) state_class = state.attributes.get("state_class") @@ -211,8 +226,12 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: if source["type"] == "grid": for flow in source["flow_from"]: - _async_validate_energy_stat( - hass, flow["stat_energy_from"], source_result + _async_validate_usage_stat( + hass, + flow["stat_energy_from"], + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, ) if flow.get("stat_cost") is not None: @@ -229,7 +248,13 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) for flow in source["flow_to"]: - _async_validate_energy_stat(hass, flow["stat_energy_to"], source_result) + _async_validate_usage_stat( + hass, + flow["stat_energy_to"], + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) if flow.get("stat_compensation") is not None: _async_validate_cost_stat( @@ -247,7 +272,13 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) elif source["type"] == "gas": - _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + _async_validate_usage_stat( + hass, + source["stat_energy_from"], + GAS_USAGE_UNITS, + GAS_UNIT_ERROR, + source_result, + ) if source.get("stat_cost") is not None: _async_validate_cost_stat(hass, source["stat_cost"], source_result) @@ -263,15 +294,39 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) elif source["type"] == "solar": - _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) + _async_validate_usage_stat( + hass, + source["stat_energy_from"], + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) elif source["type"] == "battery": - _async_validate_energy_stat(hass, source["stat_energy_from"], source_result) - _async_validate_energy_stat(hass, source["stat_energy_to"], source_result) + _async_validate_usage_stat( + hass, + source["stat_energy_from"], + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) + _async_validate_usage_stat( + hass, + source["stat_energy_to"], + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + source_result, + ) for device in manager.data["device_consumption"]: device_result: list[ValidationIssue] = [] result.device_consumption.append(device_result) - _async_validate_energy_stat(hass, device["stat_consumption"], device_result) + _async_validate_usage_stat( + hass, + device["stat_consumption"], + ENERGY_USAGE_UNITS, + ENERGY_UNIT_ERROR, + device_result, + ) return result diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 9a0b2105007..31f40bd24ea 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -441,3 +441,59 @@ async def test_validation_grid_price_errors( ], "device_consumption": [], } + + +async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded): + """Test validating gas with sensors for energy and cost/compensation.""" + mock_is_entity_recorded["sensor.gas_cost_1"] = False + mock_is_entity_recorded["sensor.gas_compensation_1"] = False + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption_1", + "stat_cost": "sensor.gas_cost_1", + }, + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption_2", + "stat_cost": "sensor.gas_cost_2", + }, + ] + } + ) + hass.states.async_set( + "sensor.gas_consumption_1", + "10.10", + {"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"}, + ) + hass.states.async_set( + "sensor.gas_cost_2", + "10.10", + {"unit_of_measurement": "EUR/kWh", "state_class": "total_increasing"}, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_gas", + "identifier": "sensor.gas_consumption_1", + "value": "beers", + }, + { + "type": "recorder_untracked", + "identifier": "sensor.gas_cost_1", + "value": None, + }, + ], + [], + ], + "device_consumption": [], + }