Enforce device class for gas and energy sensors used by energy dashboard (#56218)

* Enforce device class for gas and energy sensors used by energy dashboard

* Adjust tests
This commit is contained in:
Erik Montnemery 2021-09-14 16:56:36 +02:00 committed by GitHub
parent aaa62dadec
commit bac55b78fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 128 additions and 27 deletions

View File

@ -1,12 +1,13 @@
"""Validate the energy preferences provide valid data.""" """Validate the energy preferences provide valid data."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence from collections.abc import Mapping, Sequence
import dataclasses import dataclasses
from typing import Any from typing import Any
from homeassistant.components import recorder, sensor from homeassistant.components import recorder, sensor
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
@ -19,14 +20,16 @@ from homeassistant.core import HomeAssistant, callback, valid_entity_id
from . import data from . import data
from .const import DOMAIN 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" ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy"
GAS_USAGE_UNITS = ( GAS_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY, sensor.DEVICE_CLASS_GAS)
ENERGY_WATT_HOUR, GAS_USAGE_UNITS = {
ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR),
VOLUME_CUBIC_METERS, sensor.DEVICE_CLASS_GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET),
VOLUME_CUBIC_FEET, }
)
GAS_UNIT_ERROR = "entity_unexpected_unit_gas" GAS_UNIT_ERROR = "entity_unexpected_unit_gas"
@ -59,7 +62,8 @@ class EnergyPreferencesValidation:
def _async_validate_usage_stat( def _async_validate_usage_stat(
hass: HomeAssistant, hass: HomeAssistant,
stat_value: str, stat_value: str,
allowed_units: Sequence[str], allowed_device_classes: Sequence[str],
allowed_units: Mapping[str, Sequence[str]],
unit_error: str, unit_error: str,
result: list[ValidationIssue], result: list[ValidationIssue],
) -> None: ) -> None:
@ -106,19 +110,29 @@ def _async_validate_usage_stat(
ValidationIssue("entity_negative_state", stat_value, current_value) 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: if device_class and unit not in allowed_units.get(device_class, []):
result.append(ValidationIssue(unit_error, stat_value, unit)) 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_MEASUREMENT,
sensor.STATE_CLASS_TOTAL, sensor.STATE_CLASS_TOTAL,
sensor.STATE_CLASS_TOTAL_INCREASING, sensor.STATE_CLASS_TOTAL_INCREASING,
] ]
if state_class not in supported_state_classes: if state_class not in allowed_state_classes:
result.append( result.append(
ValidationIssue( ValidationIssue(
"entity_unexpected_state_class_total_increasing", "entity_unexpected_state_class_total_increasing",
@ -236,6 +250,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
flow["stat_energy_from"], flow["stat_energy_from"],
ENERGY_USAGE_DEVICE_CLASSES,
ENERGY_USAGE_UNITS, ENERGY_USAGE_UNITS,
ENERGY_UNIT_ERROR, ENERGY_UNIT_ERROR,
source_result, source_result,
@ -258,6 +273,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
flow["stat_energy_to"], flow["stat_energy_to"],
ENERGY_USAGE_DEVICE_CLASSES,
ENERGY_USAGE_UNITS, ENERGY_USAGE_UNITS,
ENERGY_UNIT_ERROR, ENERGY_UNIT_ERROR,
source_result, source_result,
@ -282,6 +298,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
source["stat_energy_from"], source["stat_energy_from"],
GAS_USAGE_DEVICE_CLASSES,
GAS_USAGE_UNITS, GAS_USAGE_UNITS,
GAS_UNIT_ERROR, GAS_UNIT_ERROR,
source_result, source_result,
@ -304,6 +321,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
source["stat_energy_from"], source["stat_energy_from"],
ENERGY_USAGE_DEVICE_CLASSES,
ENERGY_USAGE_UNITS, ENERGY_USAGE_UNITS,
ENERGY_UNIT_ERROR, ENERGY_UNIT_ERROR,
source_result, source_result,
@ -313,6 +331,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
source["stat_energy_from"], source["stat_energy_from"],
ENERGY_USAGE_DEVICE_CLASSES,
ENERGY_USAGE_UNITS, ENERGY_USAGE_UNITS,
ENERGY_UNIT_ERROR, ENERGY_UNIT_ERROR,
source_result, source_result,
@ -320,6 +339,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
source["stat_energy_to"], source["stat_energy_to"],
ENERGY_USAGE_DEVICE_CLASSES,
ENERGY_USAGE_UNITS, ENERGY_USAGE_UNITS,
ENERGY_UNIT_ERROR, ENERGY_UNIT_ERROR,
source_result, source_result,
@ -331,6 +351,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
_async_validate_usage_stat( _async_validate_usage_stat(
hass, hass,
device["stat_consumption"], device["stat_consumption"],
ENERGY_USAGE_DEVICE_CLASSES,
ENERGY_USAGE_UNITS, ENERGY_USAGE_UNITS,
ENERGY_UNIT_ERROR, ENERGY_UNIT_ERROR,
device_result, device_result,

View File

@ -45,7 +45,11 @@ async def test_validation(hass, mock_energy_manager):
hass.states.async_set( hass.states.async_set(
f"sensor.{key}", f"sensor.{key}",
"123", "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( await mock_energy_manager.async_update(
@ -142,7 +146,11 @@ async def test_validation_device_consumption_entity_unexpected_unit(
hass.states.async_set( hass.states.async_set(
"sensor.unexpected_unit", "sensor.unexpected_unit",
"10.10", "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() == { 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( hass.states.async_set(
"sensor.solar_production", "sensor.solar_production",
"10.10", "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() == { 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( hass.states.async_set(
"sensor.battery_import", "sensor.battery_import",
"10.10", "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( hass.states.async_set(
"sensor.battery_export", "sensor.battery_export",
"10.10", "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() == { 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( hass.states.async_set(
"sensor.grid_consumption_1", "sensor.grid_consumption_1",
"10.10", "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( hass.states.async_set(
"sensor.grid_production_1", "sensor.grid_production_1",
"10.10", "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() == { 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( hass.states.async_set(
"sensor.grid_consumption_1", "sensor.grid_consumption_1",
"10.10", "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( hass.states.async_set(
"sensor.grid_production_1", "sensor.grid_production_1",
"10.10", "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( await mock_energy_manager.async_update(
{ {
@ -402,7 +438,11 @@ async def test_validation_grid_price_errors(
hass.states.async_set( hass.states.async_set(
"sensor.grid_consumption_1", "sensor.grid_consumption_1",
"10.10", "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( hass.states.async_set(
"sensor.grid_price_1", "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_energy_from": "sensor.gas_consumption_2",
"stat_cost": "sensor.gas_cost_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( hass.states.async_set(
"sensor.gas_consumption_1", "sensor.gas_consumption_1",
"10.10", "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( hass.states.async_set(
"sensor.gas_consumption_2", "sensor.gas_consumption_2",
"10.10", "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": "",
"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( hass.states.async_set(
"sensor.gas_cost_2", "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": [], "device_consumption": [],
} }