Refactor energy validation issue reporting (#85523)

* Refactor energy validation issue reporting

* Update English translations

* Adjust translations
This commit is contained in:
Erik Montnemery 2023-01-12 12:50:43 +01:00 committed by GitHub
parent e4e96d3394
commit 8418a30cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 311 additions and 173 deletions

View File

@ -1,3 +1,61 @@
{ {
"title": "Energy" "title": "Energy",
"issues": {
"entity_not_defined": {
"title": "Entity not defined",
"description": "Check the integration or your configuration that provides:"
},
"recorder_untracked": {
"title": "Entity not tracked",
"description": "The recorder has been configured to exclude these configured entities:"
},
"entity_unavailable": {
"title": "Entity unavailable",
"description": "The state of these configured entities are currently not available:"
},
"entity_state_non_numeric": {
"title": "Entity has non-numeric state",
"description": "The following entities have a state that cannot be parsed as a number:"
},
"entity_negative_state": {
"title": "Entity has a negative state",
"description": "The following entities have a negative state while a positive state is expected:"
},
"entity_unexpected_unit_energy": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have an expected unit of measurement (either of {energy_units}):"
},
"entity_unexpected_unit_gas": {
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]",
"description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor:)"
},
"entity_unexpected_unit_water": {
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]",
"description": "The following entities do not have the expected unit of measurement (either of {water_units}):"
},
"entity_unexpected_unit_energy_price": {
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]",
"description": "The following entities do not have an expected unit of measurement {price_units}:"
},
"entity_unexpected_unit_gas_price": {
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::title%]",
"description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]"
},
"entity_unexpected_unit_water_price": {
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]",
"description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]"
},
"entity_unexpected_state_class": {
"title": "Unexpected state class",
"description": "The following entities do not have the expected state class:"
},
"entity_unexpected_device_class": {
"title": "Unexpected device class",
"description": "The following entities do not have the expected device class:"
},
"entity_state_class_measurement_no_last_reset": {
"title": "Last reset missing",
"description": "The following entities have state class 'measurement' but 'last_reset' is missing:"
}
}
} }

View File

@ -1,3 +1,61 @@
{ {
"issues": {
"entity_negative_state": {
"description": "The following entities have a negative state while a positive state is expected:",
"title": "Entity has a negative state"
},
"entity_not_defined": {
"description": "Check the integration or your configuration that provides:",
"title": "Entity not defined"
},
"entity_state_class_measurement_no_last_reset": {
"description": "The following entities have state class 'measurement' but 'last_reset' is missing:",
"title": "Last reset missing"
},
"entity_state_non_numeric": {
"description": "The following entities have a state that cannot be parsed as a number:",
"title": "Entity has non-numeric state"
},
"entity_unavailable": {
"description": "The state of these configured entities are currently not available:",
"title": "Entity unavailable"
},
"entity_unexpected_device_class": {
"description": "The following entities do not have the expected device class:",
"title": "Unexpected device class"
},
"entity_unexpected_state_class": {
"description": "The following entities do not have the expected state class:",
"title": "Unexpected state class"
},
"entity_unexpected_unit_energy": {
"description": "The following entities do not have an expected unit of measurement (either of {energy_units}):",
"title": "Unexpected unit of measurement"
},
"entity_unexpected_unit_energy_price": {
"description": "The following entities do not have an expected unit of measurement {price_units}:",
"title": "Unexpected unit of measurement"
},
"entity_unexpected_unit_gas": {
"description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor:)",
"title": "Unexpected unit of measurement"
},
"entity_unexpected_unit_gas_price": {
"description": "The following entities do not have an expected unit of measurement (either of {energy_units}):",
"title": "Unexpected unit of measurement"
},
"entity_unexpected_unit_water": {
"description": "The following entities do not have the expected unit of measurement (either of {water_units}):",
"title": "Unexpected unit of measurement"
},
"entity_unexpected_unit_water_price": {
"description": "The following entities do not have an expected unit of measurement (either of {energy_units}):",
"title": "Unexpected unit of measurement"
},
"recorder_untracked": {
"description": "The recorder has been configured to exclude these configured entities:",
"title": "Entity not tracked"
}
},
"title": "Energy" "title": "Energy"
} }

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
import dataclasses import dataclasses
import functools import functools
from typing import Any
from homeassistant.components import recorder, sensor from homeassistant.components import recorder, sensor
from homeassistant.const import ( from homeassistant.const import (
@ -72,29 +71,94 @@ WATER_UNIT_ERROR = "entity_unexpected_unit_water"
WATER_PRICE_UNIT_ERROR = "entity_unexpected_unit_water_price" WATER_PRICE_UNIT_ERROR = "entity_unexpected_unit_water_price"
def _get_placeholders(hass: HomeAssistant, issue_type: str) -> dict[str, str] | None:
currency = hass.config.currency
if issue_type == ENERGY_UNIT_ERROR:
return {
"energy_units": ", ".join(
ENERGY_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY]
),
}
if issue_type == ENERGY_PRICE_UNIT_ERROR:
return {
"price_units": ", ".join(
f"{currency}{unit}" for unit in ENERGY_PRICE_UNITS
),
}
if issue_type == GAS_UNIT_ERROR:
return {
"energy_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY]),
"gas_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.GAS]),
}
if issue_type == GAS_PRICE_UNIT_ERROR:
return {
"price_units": ", ".join(f"{currency}{unit}" for unit in GAS_PRICE_UNITS),
}
if issue_type == WATER_UNIT_ERROR:
return {
"water_units": ", ".join(WATER_USAGE_UNITS[sensor.SensorDeviceClass.WATER]),
}
if issue_type == WATER_PRICE_UNIT_ERROR:
return {
"price_units": ", ".join(f"{currency}{unit}" for unit in WATER_PRICE_UNITS),
}
return None
@dataclasses.dataclass @dataclasses.dataclass
class ValidationIssue: class ValidationIssue:
"""Error or warning message.""" """Error or warning message."""
type: str type: str
identifier: str affected_entities: set[tuple[str, float | str | None]] = dataclasses.field(
value: Any | None = None default_factory=set
)
translation_placeholders: dict[str, str] | None = None
@dataclasses.dataclass
class ValidationIssues:
"""Container for validation issues."""
issues: dict[str, ValidationIssue] = dataclasses.field(default_factory=dict)
def __init__(self) -> None:
"""Container for validiation issues."""
self.issues = {}
def add_issue(
self,
hass: HomeAssistant,
issue_type: str,
affected_entity: str,
detail: float | str | None = None,
) -> None:
"""Add an issue for an entity."""
if not (issue := self.issues.get(issue_type)):
self.issues[issue_type] = issue = ValidationIssue(issue_type)
issue.translation_placeholders = _get_placeholders(hass, issue_type)
issue.affected_entities.add((affected_entity, detail))
@dataclasses.dataclass @dataclasses.dataclass
class EnergyPreferencesValidation: class EnergyPreferencesValidation:
"""Dictionary holding validation information.""" """Dictionary holding validation information."""
energy_sources: list[list[ValidationIssue]] = dataclasses.field( energy_sources: list[ValidationIssues] = dataclasses.field(default_factory=list)
default_factory=list device_consumption: list[ValidationIssues] = dataclasses.field(default_factory=list)
)
device_consumption: list[list[ValidationIssue]] = dataclasses.field(
default_factory=list
)
def as_dict(self) -> dict: def as_dict(self) -> dict:
"""Return dictionary version.""" """Return dictionary version."""
return dataclasses.asdict(self) return {
"energy_sources": [
[dataclasses.asdict(issue) for issue in issues.issues.values()]
for issues in self.energy_sources
],
"device_consumption": [
[dataclasses.asdict(issue) for issue in issues.issues.values()]
for issues in self.device_consumption
],
}
@callback @callback
@ -105,11 +169,11 @@ def _async_validate_usage_stat(
allowed_device_classes: Sequence[str], allowed_device_classes: Sequence[str],
allowed_units: Mapping[str, Sequence[str]], allowed_units: Mapping[str, Sequence[str]],
unit_error: str, unit_error: str,
result: list[ValidationIssue], issues: ValidationIssues,
) -> None: ) -> None:
"""Validate a statistic.""" """Validate a statistic."""
if stat_id not in metadata: if stat_id not in metadata:
result.append(ValidationIssue("statistics_not_defined", stat_id)) issues.add_issue(hass, "statistics_not_defined", stat_id)
has_entity_source = valid_entity_id(stat_id) has_entity_source = valid_entity_id(stat_id)
@ -119,54 +183,36 @@ def _async_validate_usage_stat(
entity_id = stat_id entity_id = stat_id
if not recorder.is_entity_recorded(hass, entity_id): if not recorder.is_entity_recorded(hass, entity_id):
result.append( issues.add_issue(hass, "recorder_untracked", entity_id)
ValidationIssue(
"recorder_untracked",
entity_id,
)
)
return return
if (state := hass.states.get(entity_id)) is None: if (state := hass.states.get(entity_id)) is None:
result.append( issues.add_issue(hass, "entity_not_defined", entity_id)
ValidationIssue(
"entity_not_defined",
entity_id,
)
)
return return
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
result.append(ValidationIssue("entity_unavailable", entity_id, state.state)) issues.add_issue(hass, "entity_unavailable", entity_id, state.state)
return return
try: try:
current_value: float | None = float(state.state) current_value: float | None = float(state.state)
except ValueError: except ValueError:
result.append( issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state)
ValidationIssue("entity_state_non_numeric", entity_id, state.state)
)
return return
if current_value is not None and current_value < 0: if current_value is not None and current_value < 0:
result.append( issues.add_issue(hass, "entity_negative_state", entity_id, current_value)
ValidationIssue("entity_negative_state", entity_id, current_value)
)
device_class = state.attributes.get(ATTR_DEVICE_CLASS) device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class not in allowed_device_classes: if device_class not in allowed_device_classes:
result.append( issues.add_issue(
ValidationIssue( hass, "entity_unexpected_device_class", entity_id, device_class
"entity_unexpected_device_class",
entity_id,
device_class,
)
) )
else: else:
unit = state.attributes.get("unit_of_measurement") unit = state.attributes.get("unit_of_measurement")
if device_class and unit not in allowed_units.get(device_class, []): if device_class and unit not in allowed_units.get(device_class, []):
result.append(ValidationIssue(unit_error, entity_id, unit)) issues.add_issue(hass, unit_error, entity_id, unit)
state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) state_class = state.attributes.get(sensor.ATTR_STATE_CLASS)
@ -176,20 +222,14 @@ def _async_validate_usage_stat(
sensor.SensorStateClass.TOTAL_INCREASING, sensor.SensorStateClass.TOTAL_INCREASING,
] ]
if state_class not in allowed_state_classes: if state_class not in allowed_state_classes:
result.append( issues.add_issue(hass, "entity_unexpected_state_class", entity_id, state_class)
ValidationIssue(
"entity_unexpected_state_class",
entity_id,
state_class,
)
)
if ( if (
state_class == sensor.SensorStateClass.MEASUREMENT state_class == sensor.SensorStateClass.MEASUREMENT
and sensor.ATTR_LAST_RESET not in state.attributes and sensor.ATTR_LAST_RESET not in state.attributes
): ):
result.append( issues.add_issue(
ValidationIssue("entity_state_class_measurement_no_last_reset", entity_id) hass, "entity_state_class_measurement_no_last_reset", entity_id
) )
@ -197,32 +237,25 @@ def _async_validate_usage_stat(
def _async_validate_price_entity( def _async_validate_price_entity(
hass: HomeAssistant, hass: HomeAssistant,
entity_id: str, entity_id: str,
result: list[ValidationIssue], issues: ValidationIssues,
allowed_units: tuple[str, ...], allowed_units: tuple[str, ...],
unit_error: str, unit_error: str,
) -> None: ) -> None:
"""Validate that the price entity is correct.""" """Validate that the price entity is correct."""
if (state := hass.states.get(entity_id)) is None: if (state := hass.states.get(entity_id)) is None:
result.append( issues.add_issue(hass, "entity_not_defined", entity_id)
ValidationIssue(
"entity_not_defined",
entity_id,
)
)
return return
try: try:
float(state.state) float(state.state)
except ValueError: except ValueError:
result.append( issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state)
ValidationIssue("entity_state_non_numeric", entity_id, state.state)
)
return return
unit = state.attributes.get("unit_of_measurement") unit = state.attributes.get("unit_of_measurement")
if unit is None or not unit.endswith(allowed_units): if unit is None or not unit.endswith(allowed_units):
result.append(ValidationIssue(unit_error, entity_id, unit)) issues.add_issue(hass, unit_error, entity_id, unit)
@callback @callback
@ -230,11 +263,11 @@ def _async_validate_cost_stat(
hass: HomeAssistant, hass: HomeAssistant,
metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]], metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]],
stat_id: str, stat_id: str,
result: list[ValidationIssue], issues: ValidationIssues,
) -> None: ) -> None:
"""Validate that the cost stat is correct.""" """Validate that the cost stat is correct."""
if stat_id not in metadata: if stat_id not in metadata:
result.append(ValidationIssue("statistics_not_defined", stat_id)) issues.add_issue(hass, "statistics_not_defined", stat_id)
has_entity = valid_entity_id(stat_id) has_entity = valid_entity_id(stat_id)
@ -242,10 +275,10 @@ def _async_validate_cost_stat(
return return
if not recorder.is_entity_recorded(hass, stat_id): if not recorder.is_entity_recorded(hass, stat_id):
result.append(ValidationIssue("recorder_untracked", stat_id)) issues.add_issue(hass, "recorder_untracked", stat_id)
if (state := hass.states.get(stat_id)) is None: if (state := hass.states.get(stat_id)) is None:
result.append(ValidationIssue("entity_not_defined", stat_id)) issues.add_issue(hass, "entity_not_defined", stat_id)
return return
state_class = state.attributes.get("state_class") state_class = state.attributes.get("state_class")
@ -256,22 +289,18 @@ def _async_validate_cost_stat(
sensor.SensorStateClass.TOTAL_INCREASING, sensor.SensorStateClass.TOTAL_INCREASING,
] ]
if state_class not in supported_state_classes: if state_class not in supported_state_classes:
result.append( issues.add_issue(hass, "entity_unexpected_state_class", stat_id, state_class)
ValidationIssue("entity_unexpected_state_class", stat_id, state_class)
)
if ( if (
state_class == sensor.SensorStateClass.MEASUREMENT state_class == sensor.SensorStateClass.MEASUREMENT
and sensor.ATTR_LAST_RESET not in state.attributes and sensor.ATTR_LAST_RESET not in state.attributes
): ):
result.append( issues.add_issue(hass, "entity_state_class_measurement_no_last_reset", stat_id)
ValidationIssue("entity_state_class_measurement_no_last_reset", stat_id)
)
@callback @callback
def _async_validate_auto_generated_cost_entity( def _async_validate_auto_generated_cost_entity(
hass: HomeAssistant, energy_entity_id: str, result: list[ValidationIssue] hass: HomeAssistant, energy_entity_id: str, issues: ValidationIssues
) -> None: ) -> None:
"""Validate that the auto generated cost entity is correct.""" """Validate that the auto generated cost entity is correct."""
if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]: if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]:
@ -280,7 +309,7 @@ def _async_validate_auto_generated_cost_entity(
cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id] cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id]
if not recorder.is_entity_recorded(hass, cost_entity_id): if not recorder.is_entity_recorded(hass, cost_entity_id):
result.append(ValidationIssue("recorder_untracked", cost_entity_id)) issues.add_issue(hass, "recorder_untracked", cost_entity_id)
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
@ -297,7 +326,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
# Create a list of validation checks # Create a list of validation checks
for source in manager.data["energy_sources"]: for source in manager.data["energy_sources"]:
source_result: list[ValidationIssue] = [] source_result = ValidationIssues()
result.energy_sources.append(source_result) result.energy_sources.append(source_result)
if source["type"] == "grid": if source["type"] == "grid":
@ -550,7 +579,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
) )
for device in manager.data["device_consumption"]: for device in manager.data["device_consumption"]:
device_result: list[ValidationIssue] = [] device_result = ValidationIssues()
result.device_consumption.append(device_result) result.device_consumption.append(device_result)
wanted_statistics_metadata.add(device["stat_consumption"]) wanted_statistics_metadata.add(device["stat_consumption"])
validate_calls.append( validate_calls.append(

View File

@ -118,13 +118,13 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma
[ [
{ {
"type": "statistics_not_defined", "type": "statistics_not_defined",
"identifier": "sensor.not_exist", "affected_entities": {("sensor.not_exist", None)},
"value": None, "translation_placeholders": None,
}, },
{ {
"type": "entity_not_defined", "type": "entity_not_defined",
"identifier": "sensor.not_exist", "affected_entities": {("sensor.not_exist", None)},
"value": None, "translation_placeholders": None,
}, },
] ]
], ],
@ -142,8 +142,8 @@ async def test_validation_device_consumption_stat_missing(hass, mock_energy_mana
[ [
{ {
"type": "statistics_not_defined", "type": "statistics_not_defined",
"identifier": "external:not_exist", "affected_entities": {("external:not_exist", None)},
"value": None, "translation_placeholders": None,
} }
] ]
], ],
@ -165,8 +165,8 @@ async def test_validation_device_consumption_entity_unavailable(
[ [
{ {
"type": "entity_unavailable", "type": "entity_unavailable",
"identifier": "sensor.unavailable", "affected_entities": {("sensor.unavailable", "unavailable")},
"value": "unavailable", "translation_placeholders": None,
} }
] ]
], ],
@ -188,8 +188,8 @@ async def test_validation_device_consumption_entity_non_numeric(
[ [
{ {
"type": "entity_state_non_numeric", "type": "entity_state_non_numeric",
"identifier": "sensor.non_numeric", "affected_entities": {("sensor.non_numeric", "123,123.10")},
"value": "123,123.10", "translation_placeholders": None,
}, },
] ]
], ],
@ -219,8 +219,8 @@ async def test_validation_device_consumption_entity_unexpected_unit(
[ [
{ {
"type": "entity_unexpected_unit_energy", "type": "entity_unexpected_unit_energy",
"identifier": "sensor.unexpected_unit", "affected_entities": {("sensor.unexpected_unit", "beers")},
"value": "beers", "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"},
} }
] ]
], ],
@ -242,8 +242,8 @@ async def test_validation_device_consumption_recorder_not_tracked(
[ [
{ {
"type": "recorder_untracked", "type": "recorder_untracked",
"identifier": "sensor.not_recorded", "affected_entities": {("sensor.not_recorded", None)},
"value": None, "translation_placeholders": None,
} }
] ]
], ],
@ -273,8 +273,8 @@ async def test_validation_device_consumption_no_last_reset(
[ [
{ {
"type": "entity_state_class_measurement_no_last_reset", "type": "entity_state_class_measurement_no_last_reset",
"identifier": "sensor.no_last_reset", "affected_entities": {("sensor.no_last_reset", None)},
"value": None, "translation_placeholders": None,
} }
] ]
], ],
@ -305,8 +305,8 @@ async def test_validation_solar(hass, mock_energy_manager, mock_get_metadata):
[ [
{ {
"type": "entity_unexpected_unit_energy", "type": "entity_unexpected_unit_energy",
"identifier": "sensor.solar_production", "affected_entities": {("sensor.solar_production", "beers")},
"value": "beers", "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"},
} }
] ]
], ],
@ -351,13 +351,11 @@ async def test_validation_battery(hass, mock_energy_manager, mock_get_metadata):
[ [
{ {
"type": "entity_unexpected_unit_energy", "type": "entity_unexpected_unit_energy",
"identifier": "sensor.battery_import", "affected_entities": {
"value": "beers", ("sensor.battery_import", "beers"),
}, ("sensor.battery_export", "beers"),
{ },
"type": "entity_unexpected_unit_energy", "translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"},
"identifier": "sensor.battery_export",
"value": "beers",
}, },
] ]
], ],
@ -422,43 +420,35 @@ async def test_validation_grid(
[ [
{ {
"type": "entity_unexpected_unit_energy", "type": "entity_unexpected_unit_energy",
"identifier": "sensor.grid_consumption_1", "affected_entities": {
"value": "beers", ("sensor.grid_consumption_1", "beers"),
("sensor.grid_production_1", "beers"),
},
"translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"},
}, },
{ {
"type": "statistics_not_defined", "type": "statistics_not_defined",
"identifier": "sensor.grid_cost_1", "affected_entities": {
"value": None, ("sensor.grid_cost_1", None),
("sensor.grid_compensation_1", None),
},
"translation_placeholders": None,
}, },
{ {
"type": "recorder_untracked", "type": "recorder_untracked",
"identifier": "sensor.grid_cost_1", "affected_entities": {
"value": None, ("sensor.grid_cost_1", None),
("sensor.grid_compensation_1", None),
},
"translation_placeholders": None,
}, },
{ {
"type": "entity_not_defined", "type": "entity_not_defined",
"identifier": "sensor.grid_cost_1", "affected_entities": {
"value": None, ("sensor.grid_cost_1", None),
}, ("sensor.grid_compensation_1", None),
{ },
"type": "entity_unexpected_unit_energy", "translation_placeholders": None,
"identifier": "sensor.grid_production_1",
"value": "beers",
},
{
"type": "statistics_not_defined",
"identifier": "sensor.grid_compensation_1",
"value": None,
},
{
"type": "recorder_untracked",
"identifier": "sensor.grid_compensation_1",
"value": None,
},
{
"type": "entity_not_defined",
"identifier": "sensor.grid_compensation_1",
"value": None,
}, },
] ]
], ],
@ -517,23 +507,19 @@ async def test_validation_grid_external_cost_compensation(
[ [
{ {
"type": "entity_unexpected_unit_energy", "type": "entity_unexpected_unit_energy",
"identifier": "sensor.grid_consumption_1", "affected_entities": {
"value": "beers", ("sensor.grid_consumption_1", "beers"),
("sensor.grid_production_1", "beers"),
},
"translation_placeholders": {"energy_units": "GJ, kWh, MWh, Wh"},
}, },
{ {
"type": "statistics_not_defined", "type": "statistics_not_defined",
"identifier": "external:grid_cost_1", "affected_entities": {
"value": None, ("external:grid_cost_1", None),
}, ("external:grid_compensation_1", None),
{ },
"type": "entity_unexpected_unit_energy", "translation_placeholders": None,
"identifier": "sensor.grid_production_1",
"value": "beers",
},
{
"type": "statistics_not_defined",
"identifier": "external:grid_compensation_1",
"value": None,
}, },
] ]
], ],
@ -599,18 +585,16 @@ async def test_validation_grid_price_not_exist(
[ [
{ {
"type": "entity_not_defined", "type": "entity_not_defined",
"identifier": "sensor.grid_price_1", "affected_entities": {("sensor.grid_price_1", None)},
"value": None, "translation_placeholders": None,
}, },
{ {
"type": "recorder_untracked", "type": "recorder_untracked",
"identifier": "sensor.grid_consumption_1_cost", "affected_entities": {
"value": None, ("sensor.grid_consumption_1_cost", None),
}, ("sensor.grid_production_1_compensation", None),
{ },
"type": "recorder_untracked", "translation_placeholders": None,
"identifier": "sensor.grid_production_1_compensation",
"value": None,
}, },
] ]
], ],
@ -683,8 +667,8 @@ async def test_validation_grid_auto_cost_entity_errors(
"$/kWh", "$/kWh",
{ {
"type": "entity_state_non_numeric", "type": "entity_state_non_numeric",
"identifier": "sensor.grid_price_1", "affected_entities": {("sensor.grid_price_1", "123,123.12")},
"value": "123,123.12", "translation_placeholders": None,
}, },
), ),
( (
@ -692,8 +676,10 @@ async def test_validation_grid_auto_cost_entity_errors(
"$/Ws", "$/Ws",
{ {
"type": "entity_unexpected_unit_energy_price", "type": "entity_unexpected_unit_energy_price",
"identifier": "sensor.grid_price_1", "affected_entities": {("sensor.grid_price_1", "$/Ws")},
"value": "$/Ws", "translation_placeholders": {
"price_units": "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh"
},
}, },
), ),
), ),
@ -834,18 +820,21 @@ async def test_validation_gas(
[ [
{ {
"type": "entity_unexpected_unit_gas", "type": "entity_unexpected_unit_gas",
"identifier": "sensor.gas_consumption_1", "affected_entities": {("sensor.gas_consumption_1", "beers")},
"value": "beers", "translation_placeholders": {
"energy_units": "GJ, kWh, MWh, Wh",
"gas_units": "CCF, ft³, m³",
},
}, },
{ {
"type": "recorder_untracked", "type": "recorder_untracked",
"identifier": "sensor.gas_cost_1", "affected_entities": {("sensor.gas_cost_1", None)},
"value": None, "translation_placeholders": None,
}, },
{ {
"type": "entity_not_defined", "type": "entity_not_defined",
"identifier": "sensor.gas_cost_1", "affected_entities": {("sensor.gas_cost_1", None)},
"value": None, "translation_placeholders": None,
}, },
], ],
[], [],
@ -853,15 +842,17 @@ async def test_validation_gas(
[ [
{ {
"type": "entity_unexpected_device_class", "type": "entity_unexpected_device_class",
"identifier": "sensor.gas_consumption_4", "affected_entities": {("sensor.gas_consumption_4", None)},
"value": None, "translation_placeholders": None,
}, },
], ],
[ [
{ {
"type": "entity_unexpected_unit_gas_price", "type": "entity_unexpected_unit_gas_price",
"identifier": "sensor.gas_price_2", "affected_entities": {("sensor.gas_price_2", "EUR/invalid")},
"value": "EUR/invalid", "translation_placeholders": {
"price_units": "EUR/GJ, EUR/kWh, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³"
},
}, },
], ],
], ],
@ -1039,18 +1030,18 @@ async def test_validation_water(
[ [
{ {
"type": "entity_unexpected_unit_water", "type": "entity_unexpected_unit_water",
"identifier": "sensor.water_consumption_1", "affected_entities": {("sensor.water_consumption_1", "beers")},
"value": "beers", "translation_placeholders": {"water_units": "CCF, ft³, m³, gal, L"},
}, },
{ {
"type": "recorder_untracked", "type": "recorder_untracked",
"identifier": "sensor.water_cost_1", "affected_entities": {("sensor.water_cost_1", None)},
"value": None, "translation_placeholders": None,
}, },
{ {
"type": "entity_not_defined", "type": "entity_not_defined",
"identifier": "sensor.water_cost_1", "affected_entities": {("sensor.water_cost_1", None)},
"value": None, "translation_placeholders": None,
}, },
], ],
[], [],
@ -1058,15 +1049,17 @@ async def test_validation_water(
[ [
{ {
"type": "entity_unexpected_device_class", "type": "entity_unexpected_device_class",
"identifier": "sensor.water_consumption_4", "affected_entities": {("sensor.water_consumption_4", None)},
"value": None, "translation_placeholders": None,
}, },
], ],
[ [
{ {
"type": "entity_unexpected_unit_water_price", "type": "entity_unexpected_unit_water_price",
"identifier": "sensor.water_price_2", "affected_entities": {("sensor.water_price_2", "EUR/invalid")},
"value": "EUR/invalid", "translation_placeholders": {
"price_units": "EUR/CCF, EUR/ft³, EUR/m³, EUR/gal, EUR/L"
},
}, },
], ],
], ],