mirror of
https://github.com/home-assistant/core
synced 2024-10-05 19:57:25 +00:00
Tweak energy validator (#58018)
* Tweak energy validator * Update code and tests * Tweak implementation * Update tests * Update after rebase
This commit is contained in:
parent
d67c1118dc
commit
547e36ae94
|
@ -23,7 +23,13 @@ from homeassistant.const import (
|
||||||
ENERGY_WATT_HOUR,
|
ENERGY_WATT_HOUR,
|
||||||
VOLUME_CUBIC_METERS,
|
VOLUME_CUBIC_METERS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
|
from homeassistant.core import (
|
||||||
|
HomeAssistant,
|
||||||
|
State,
|
||||||
|
callback,
|
||||||
|
split_entity_id,
|
||||||
|
valid_entity_id,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_track_state_change_event
|
from homeassistant.helpers.event import async_track_state_change_event
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
@ -177,9 +183,13 @@ class SensorManager:
|
||||||
|
|
||||||
# Make sure the right data is there
|
# Make sure the right data is there
|
||||||
# If the entity existed, we don't pop it from to_remove so it's removed
|
# If the entity existed, we don't pop it from to_remove so it's removed
|
||||||
if config.get(adapter.entity_energy_key) is None or (
|
if (
|
||||||
config.get("entity_energy_price") is None
|
config.get(adapter.entity_energy_key) is None
|
||||||
and config.get("number_energy_price") is None
|
or not valid_entity_id(config[adapter.entity_energy_key])
|
||||||
|
or (
|
||||||
|
config.get("entity_energy_price") is None
|
||||||
|
and config.get("number_energy_price") is None
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import functools
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components import recorder, sensor
|
from homeassistant.components import recorder, sensor
|
||||||
|
@ -66,56 +67,68 @@ class EnergyPreferencesValidation:
|
||||||
return dataclasses.asdict(self)
|
return dataclasses.asdict(self)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
async def _async_validate_usage_stat(
|
||||||
def _async_validate_usage_stat(
|
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
stat_value: str,
|
stat_id: str,
|
||||||
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],
|
result: list[ValidationIssue],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate a statistic."""
|
"""Validate a statistic."""
|
||||||
has_entity_source = valid_entity_id(stat_value)
|
metadata = await hass.async_add_executor_job(
|
||||||
|
functools.partial(
|
||||||
|
recorder.statistics.get_metadata,
|
||||||
|
hass,
|
||||||
|
statistic_ids=(stat_id,),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if stat_id not in metadata:
|
||||||
|
result.append(ValidationIssue("statistics_not_defined", stat_id))
|
||||||
|
|
||||||
|
has_entity_source = valid_entity_id(stat_id)
|
||||||
|
|
||||||
if not has_entity_source:
|
if not has_entity_source:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not recorder.is_entity_recorded(hass, stat_value):
|
entity_id = stat_id
|
||||||
|
|
||||||
|
if not recorder.is_entity_recorded(hass, entity_id):
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
"recorder_untracked",
|
"recorder_untracked",
|
||||||
stat_value,
|
entity_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
state = hass.states.get(stat_value)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if state is None:
|
if state is None:
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
"entity_not_defined",
|
"entity_not_defined",
|
||||||
stat_value,
|
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", stat_value, state.state))
|
result.append(ValidationIssue("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(
|
result.append(
|
||||||
ValidationIssue("entity_state_non_numeric", stat_value, 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(
|
result.append(
|
||||||
ValidationIssue("entity_negative_state", stat_value, 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)
|
||||||
|
@ -123,7 +136,7 @@ def _async_validate_usage_stat(
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
"entity_unexpected_device_class",
|
"entity_unexpected_device_class",
|
||||||
stat_value,
|
entity_id,
|
||||||
device_class,
|
device_class,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -131,7 +144,7 @@ def _async_validate_usage_stat(
|
||||||
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, stat_value, unit))
|
result.append(ValidationIssue(unit_error, entity_id, unit))
|
||||||
|
|
||||||
state_class = state.attributes.get(sensor.ATTR_STATE_CLASS)
|
state_class = state.attributes.get(sensor.ATTR_STATE_CLASS)
|
||||||
|
|
||||||
|
@ -144,7 +157,7 @@ def _async_validate_usage_stat(
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue(
|
ValidationIssue(
|
||||||
"entity_unexpected_state_class",
|
"entity_unexpected_state_class",
|
||||||
stat_value,
|
entity_id,
|
||||||
state_class,
|
state_class,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -154,7 +167,7 @@ def _async_validate_usage_stat(
|
||||||
and sensor.ATTR_LAST_RESET not in state.attributes
|
and sensor.ATTR_LAST_RESET not in state.attributes
|
||||||
):
|
):
|
||||||
result.append(
|
result.append(
|
||||||
ValidationIssue("entity_state_class_measurement_no_last_reset", stat_value)
|
ValidationIssue("entity_state_class_measurement_no_last_reset", entity_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -192,33 +205,33 @@ def _async_validate_price_entity(
|
||||||
result.append(ValidationIssue(unit_error, entity_id, unit))
|
result.append(ValidationIssue(unit_error, entity_id, unit))
|
||||||
|
|
||||||
|
|
||||||
@callback
|
async def _async_validate_cost_stat(
|
||||||
def _async_validate_cost_stat(
|
|
||||||
hass: HomeAssistant, stat_id: str, result: list[ValidationIssue]
|
hass: HomeAssistant, stat_id: str, result: list[ValidationIssue]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate that the cost stat is correct."""
|
"""Validate that the cost stat is correct."""
|
||||||
|
metadata = await hass.async_add_executor_job(
|
||||||
|
functools.partial(
|
||||||
|
recorder.statistics.get_metadata,
|
||||||
|
hass,
|
||||||
|
statistic_ids=(stat_id,),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if stat_id not in metadata:
|
||||||
|
result.append(ValidationIssue("statistics_not_defined", stat_id))
|
||||||
|
|
||||||
has_entity = valid_entity_id(stat_id)
|
has_entity = valid_entity_id(stat_id)
|
||||||
|
|
||||||
if not has_entity:
|
if not has_entity:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not recorder.is_entity_recorded(hass, stat_id):
|
if not recorder.is_entity_recorded(hass, stat_id):
|
||||||
result.append(
|
result.append(ValidationIssue("recorder_untracked", stat_id))
|
||||||
ValidationIssue(
|
|
||||||
"recorder_untracked",
|
|
||||||
stat_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
state = hass.states.get(stat_id)
|
state = hass.states.get(stat_id)
|
||||||
|
|
||||||
if state is None:
|
if state is None:
|
||||||
result.append(
|
result.append(ValidationIssue("entity_not_defined", stat_id))
|
||||||
ValidationIssue(
|
|
||||||
"entity_not_defined",
|
|
||||||
stat_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
state_class = state.attributes.get("state_class")
|
state_class = state.attributes.get("state_class")
|
||||||
|
@ -244,16 +257,16 @@ def _async_validate_cost_stat(
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_validate_auto_generated_cost_entity(
|
def _async_validate_auto_generated_cost_entity(
|
||||||
hass: HomeAssistant, entity_id: str, result: list[ValidationIssue]
|
hass: HomeAssistant, energy_entity_id: str, result: list[ValidationIssue]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate that the auto generated cost entity is correct."""
|
"""Validate that the auto generated cost entity is correct."""
|
||||||
if not recorder.is_entity_recorded(hass, entity_id):
|
if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]:
|
||||||
result.append(
|
# The cost entity has not been setup
|
||||||
ValidationIssue(
|
return
|
||||||
"recorder_untracked",
|
|
||||||
entity_id,
|
cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id]
|
||||||
)
|
if not recorder.is_entity_recorded(hass, cost_entity_id):
|
||||||
)
|
result.append(ValidationIssue("recorder_untracked", cost_entity_id))
|
||||||
|
|
||||||
|
|
||||||
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
|
@ -271,7 +284,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
|
|
||||||
if source["type"] == "grid":
|
if source["type"] == "grid":
|
||||||
for flow in source["flow_from"]:
|
for flow in source["flow_from"]:
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
flow["stat_energy_from"],
|
flow["stat_energy_from"],
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
@ -281,7 +294,9 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
)
|
)
|
||||||
|
|
||||||
if flow.get("stat_cost") is not None:
|
if flow.get("stat_cost") is not None:
|
||||||
_async_validate_cost_stat(hass, flow["stat_cost"], source_result)
|
await _async_validate_cost_stat(
|
||||||
|
hass, flow["stat_cost"], source_result
|
||||||
|
)
|
||||||
elif flow.get("entity_energy_price") is not None:
|
elif flow.get("entity_energy_price") is not None:
|
||||||
_async_validate_price_entity(
|
_async_validate_price_entity(
|
||||||
hass,
|
hass,
|
||||||
|
@ -291,18 +306,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
ENERGY_PRICE_UNIT_ERROR,
|
ENERGY_PRICE_UNIT_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if flow.get("entity_energy_from") is not None and (
|
||||||
flow.get("entity_energy_price") is not None
|
flow.get("entity_energy_price") is not None
|
||||||
or flow.get("number_energy_price") is not None
|
or flow.get("number_energy_price") is not None
|
||||||
):
|
):
|
||||||
_async_validate_auto_generated_cost_entity(
|
_async_validate_auto_generated_cost_entity(
|
||||||
hass,
|
hass,
|
||||||
hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_from"]],
|
flow["entity_energy_from"],
|
||||||
source_result,
|
source_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
for flow in source["flow_to"]:
|
for flow in source["flow_to"]:
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
flow["stat_energy_to"],
|
flow["stat_energy_to"],
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
@ -312,7 +327,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
)
|
)
|
||||||
|
|
||||||
if flow.get("stat_compensation") is not None:
|
if flow.get("stat_compensation") is not None:
|
||||||
_async_validate_cost_stat(
|
await _async_validate_cost_stat(
|
||||||
hass, flow["stat_compensation"], source_result
|
hass, flow["stat_compensation"], source_result
|
||||||
)
|
)
|
||||||
elif flow.get("entity_energy_price") is not None:
|
elif flow.get("entity_energy_price") is not None:
|
||||||
|
@ -324,18 +339,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
ENERGY_PRICE_UNIT_ERROR,
|
ENERGY_PRICE_UNIT_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if flow.get("entity_energy_to") is not None and (
|
||||||
flow.get("entity_energy_price") is not None
|
flow.get("entity_energy_price") is not None
|
||||||
or flow.get("number_energy_price") is not None
|
or flow.get("number_energy_price") is not None
|
||||||
):
|
):
|
||||||
_async_validate_auto_generated_cost_entity(
|
_async_validate_auto_generated_cost_entity(
|
||||||
hass,
|
hass,
|
||||||
hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_to"]],
|
flow["entity_energy_to"],
|
||||||
source_result,
|
source_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif source["type"] == "gas":
|
elif source["type"] == "gas":
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
source["stat_energy_from"],
|
source["stat_energy_from"],
|
||||||
GAS_USAGE_DEVICE_CLASSES,
|
GAS_USAGE_DEVICE_CLASSES,
|
||||||
|
@ -345,7 +360,9 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
)
|
)
|
||||||
|
|
||||||
if source.get("stat_cost") is not None:
|
if source.get("stat_cost") is not None:
|
||||||
_async_validate_cost_stat(hass, source["stat_cost"], source_result)
|
await _async_validate_cost_stat(
|
||||||
|
hass, source["stat_cost"], source_result
|
||||||
|
)
|
||||||
elif source.get("entity_energy_price") is not None:
|
elif source.get("entity_energy_price") is not None:
|
||||||
_async_validate_price_entity(
|
_async_validate_price_entity(
|
||||||
hass,
|
hass,
|
||||||
|
@ -355,18 +372,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
GAS_PRICE_UNIT_ERROR,
|
GAS_PRICE_UNIT_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if source.get("entity_energy_from") is not None and (
|
||||||
source.get("entity_energy_price") is not None
|
source.get("entity_energy_price") is not None
|
||||||
or source.get("number_energy_price") is not None
|
or source.get("number_energy_price") is not None
|
||||||
):
|
):
|
||||||
_async_validate_auto_generated_cost_entity(
|
_async_validate_auto_generated_cost_entity(
|
||||||
hass,
|
hass,
|
||||||
hass.data[DOMAIN]["cost_sensors"][source["stat_energy_from"]],
|
source["entity_energy_from"],
|
||||||
source_result,
|
source_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif source["type"] == "solar":
|
elif source["type"] == "solar":
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
source["stat_energy_from"],
|
source["stat_energy_from"],
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
@ -376,7 +393,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
)
|
)
|
||||||
|
|
||||||
elif source["type"] == "battery":
|
elif source["type"] == "battery":
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
source["stat_energy_from"],
|
source["stat_energy_from"],
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
@ -384,7 +401,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||||
ENERGY_UNIT_ERROR,
|
ENERGY_UNIT_ERROR,
|
||||||
source_result,
|
source_result,
|
||||||
)
|
)
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
source["stat_energy_to"],
|
source["stat_energy_to"],
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
@ -396,7 +413,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: list[ValidationIssue] = []
|
||||||
result.device_consumption.append(device_result)
|
result.device_consumption.append(device_result)
|
||||||
_async_validate_usage_stat(
|
await _async_validate_usage_stat(
|
||||||
hass,
|
hass,
|
||||||
device["stat_consumption"],
|
device["stat_consumption"],
|
||||||
ENERGY_USAGE_DEVICE_CLASSES,
|
ENERGY_USAGE_DEVICE_CLASSES,
|
||||||
|
|
|
@ -21,6 +21,20 @@ def mock_is_entity_recorded():
|
||||||
yield mocks
|
yield mocks
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_get_metadata():
|
||||||
|
"""Mock recorder.statistics.get_metadata."""
|
||||||
|
mocks = {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.statistics.get_metadata",
|
||||||
|
side_effect=lambda hass, statistic_ids: mocks.get(
|
||||||
|
statistic_ids[0], {statistic_ids[0]: (1, {})}
|
||||||
|
),
|
||||||
|
):
|
||||||
|
yield mocks
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
async def mock_energy_manager(hass):
|
async def mock_energy_manager(hass):
|
||||||
"""Set up energy."""
|
"""Set up energy."""
|
||||||
|
@ -48,7 +62,9 @@ async def test_validation_empty_config(hass):
|
||||||
("measurement", {"last_reset": "abc"}),
|
("measurement", {"last_reset": "abc"}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_validation(hass, mock_energy_manager, state_class, extra):
|
async def test_validation(
|
||||||
|
hass, mock_energy_manager, mock_get_metadata, state_class, extra
|
||||||
|
):
|
||||||
"""Test validating success."""
|
"""Test validating success."""
|
||||||
for key in ("device_cons", "battery_import", "battery_export", "solar_production"):
|
for key in ("device_cons", "battery_import", "battery_export", "solar_production"):
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -82,7 +98,7 @@ async def test_validation(hass, mock_energy_manager, state_class, extra):
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_device_consumption_entity_missing(hass, mock_energy_manager):
|
async def test_validation_device_consumption_entity_missing(hass, mock_energy_manager):
|
||||||
"""Test validating missing stat for device."""
|
"""Test validating missing entity for device."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
{"device_consumption": [{"stat_consumption": "sensor.not_exist"}]}
|
{"device_consumption": [{"stat_consumption": "sensor.not_exist"}]}
|
||||||
)
|
)
|
||||||
|
@ -90,10 +106,34 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma
|
||||||
"energy_sources": [],
|
"energy_sources": [],
|
||||||
"device_consumption": [
|
"device_consumption": [
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"type": "statistics_not_defined",
|
||||||
|
"identifier": "sensor.not_exist",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "entity_not_defined",
|
"type": "entity_not_defined",
|
||||||
"identifier": "sensor.not_exist",
|
"identifier": "sensor.not_exist",
|
||||||
"value": None,
|
"value": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validation_device_consumption_stat_missing(hass, mock_energy_manager):
|
||||||
|
"""Test validating missing statistic for device with non entity stats."""
|
||||||
|
await mock_energy_manager.async_update(
|
||||||
|
{"device_consumption": [{"stat_consumption": "external:not_exist"}]}
|
||||||
|
)
|
||||||
|
assert (await validate.async_validate(hass)).as_dict() == {
|
||||||
|
"energy_sources": [],
|
||||||
|
"device_consumption": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "statistics_not_defined",
|
||||||
|
"identifier": "external:not_exist",
|
||||||
|
"value": None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
@ -101,7 +141,7 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_device_consumption_entity_unavailable(
|
async def test_validation_device_consumption_entity_unavailable(
|
||||||
hass, mock_energy_manager
|
hass, mock_energy_manager, mock_get_metadata
|
||||||
):
|
):
|
||||||
"""Test validating missing stat for device."""
|
"""Test validating missing stat for device."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
|
@ -124,7 +164,7 @@ async def test_validation_device_consumption_entity_unavailable(
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_device_consumption_entity_non_numeric(
|
async def test_validation_device_consumption_entity_non_numeric(
|
||||||
hass, mock_energy_manager
|
hass, mock_energy_manager, mock_get_metadata
|
||||||
):
|
):
|
||||||
"""Test validating missing stat for device."""
|
"""Test validating missing stat for device."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
|
@ -147,7 +187,7 @@ async def test_validation_device_consumption_entity_non_numeric(
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_device_consumption_entity_unexpected_unit(
|
async def test_validation_device_consumption_entity_unexpected_unit(
|
||||||
hass, mock_energy_manager
|
hass, mock_energy_manager, mock_get_metadata
|
||||||
):
|
):
|
||||||
"""Test validating missing stat for device."""
|
"""Test validating missing stat for device."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
|
@ -178,7 +218,7 @@ async def test_validation_device_consumption_entity_unexpected_unit(
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_device_consumption_recorder_not_tracked(
|
async def test_validation_device_consumption_recorder_not_tracked(
|
||||||
hass, mock_energy_manager, mock_is_entity_recorded
|
hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
|
||||||
):
|
):
|
||||||
"""Test validating device based on untracked entity."""
|
"""Test validating device based on untracked entity."""
|
||||||
mock_is_entity_recorded["sensor.not_recorded"] = False
|
mock_is_entity_recorded["sensor.not_recorded"] = False
|
||||||
|
@ -200,7 +240,9 @@ async def test_validation_device_consumption_recorder_not_tracked(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_device_consumption_no_last_reset(hass, mock_energy_manager):
|
async def test_validation_device_consumption_no_last_reset(
|
||||||
|
hass, mock_energy_manager, mock_get_metadata
|
||||||
|
):
|
||||||
"""Test validating device based on untracked entity."""
|
"""Test validating device based on untracked entity."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
{"device_consumption": [{"stat_consumption": "sensor.no_last_reset"}]}
|
{"device_consumption": [{"stat_consumption": "sensor.no_last_reset"}]}
|
||||||
|
@ -229,7 +271,7 @@ async def test_validation_device_consumption_no_last_reset(hass, mock_energy_man
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_solar(hass, mock_energy_manager):
|
async def test_validation_solar(hass, mock_energy_manager, mock_get_metadata):
|
||||||
"""Test validating missing stat for device."""
|
"""Test validating missing stat for device."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
{
|
{
|
||||||
|
@ -262,7 +304,7 @@ async def test_validation_solar(hass, mock_energy_manager):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_battery(hass, mock_energy_manager):
|
async def test_validation_battery(hass, mock_energy_manager, mock_get_metadata):
|
||||||
"""Test validating missing stat for device."""
|
"""Test validating missing stat for device."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
{
|
{
|
||||||
|
@ -313,10 +355,14 @@ async def test_validation_battery(hass, mock_energy_manager):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorded):
|
async def test_validation_grid(
|
||||||
|
hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
|
||||||
|
):
|
||||||
"""Test validating grid with sensors for energy and cost/compensation."""
|
"""Test validating grid with sensors for energy and cost/compensation."""
|
||||||
mock_is_entity_recorded["sensor.grid_cost_1"] = False
|
mock_is_entity_recorded["sensor.grid_cost_1"] = False
|
||||||
mock_is_entity_recorded["sensor.grid_compensation_1"] = False
|
mock_is_entity_recorded["sensor.grid_compensation_1"] = False
|
||||||
|
mock_get_metadata["sensor.grid_cost_1"] = {}
|
||||||
|
mock_get_metadata["sensor.grid_compensation_1"] = {}
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
{
|
{
|
||||||
"energy_sources": [
|
"energy_sources": [
|
||||||
|
@ -365,6 +411,11 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde
|
||||||
"identifier": "sensor.grid_consumption_1",
|
"identifier": "sensor.grid_consumption_1",
|
||||||
"value": "beers",
|
"value": "beers",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "statistics_not_defined",
|
||||||
|
"identifier": "sensor.grid_cost_1",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "recorder_untracked",
|
"type": "recorder_untracked",
|
||||||
"identifier": "sensor.grid_cost_1",
|
"identifier": "sensor.grid_cost_1",
|
||||||
|
@ -380,6 +431,11 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde
|
||||||
"identifier": "sensor.grid_production_1",
|
"identifier": "sensor.grid_production_1",
|
||||||
"value": "beers",
|
"value": "beers",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "statistics_not_defined",
|
||||||
|
"identifier": "sensor.grid_compensation_1",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "recorder_untracked",
|
"type": "recorder_untracked",
|
||||||
"identifier": "sensor.grid_compensation_1",
|
"identifier": "sensor.grid_compensation_1",
|
||||||
|
@ -396,8 +452,91 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_grid_price_not_exist(hass, mock_energy_manager):
|
async def test_validation_grid_external_cost_compensation(
|
||||||
"""Test validating grid with price entity that does not exist."""
|
hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
|
||||||
|
):
|
||||||
|
"""Test validating grid with non entity stats for energy and cost/compensation."""
|
||||||
|
mock_get_metadata["external:grid_cost_1"] = {}
|
||||||
|
mock_get_metadata["external:grid_compensation_1"] = {}
|
||||||
|
await mock_energy_manager.async_update(
|
||||||
|
{
|
||||||
|
"energy_sources": [
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"flow_from": [
|
||||||
|
{
|
||||||
|
"stat_energy_from": "sensor.grid_consumption_1",
|
||||||
|
"stat_cost": "external:grid_cost_1",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flow_to": [
|
||||||
|
{
|
||||||
|
"stat_energy_to": "sensor.grid_production_1",
|
||||||
|
"stat_compensation": "external:grid_compensation_1",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.grid_consumption_1",
|
||||||
|
"10.10",
|
||||||
|
{
|
||||||
|
"device_class": "energy",
|
||||||
|
"unit_of_measurement": "beers",
|
||||||
|
"state_class": "total_increasing",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.grid_production_1",
|
||||||
|
"10.10",
|
||||||
|
{
|
||||||
|
"device_class": "energy",
|
||||||
|
"unit_of_measurement": "beers",
|
||||||
|
"state_class": "total_increasing",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (await validate.async_validate(hass)).as_dict() == {
|
||||||
|
"energy_sources": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "entity_unexpected_unit_energy",
|
||||||
|
"identifier": "sensor.grid_consumption_1",
|
||||||
|
"value": "beers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "statistics_not_defined",
|
||||||
|
"identifier": "external:grid_cost_1",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "entity_unexpected_unit_energy",
|
||||||
|
"identifier": "sensor.grid_production_1",
|
||||||
|
"value": "beers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "statistics_not_defined",
|
||||||
|
"identifier": "external:grid_compensation_1",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"device_consumption": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validation_grid_price_not_exist(
|
||||||
|
hass, mock_energy_manager, mock_get_metadata, mock_is_entity_recorded
|
||||||
|
):
|
||||||
|
"""Test validating grid with errors.
|
||||||
|
|
||||||
|
- The price entity for the auto generated cost entity does not exist.
|
||||||
|
- The auto generated cost entities are not recorded.
|
||||||
|
"""
|
||||||
|
mock_is_entity_recorded["sensor.grid_consumption_1_cost"] = False
|
||||||
|
mock_is_entity_recorded["sensor.grid_production_1_compensation"] = False
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.grid_consumption_1",
|
"sensor.grid_consumption_1",
|
||||||
"10.10",
|
"10.10",
|
||||||
|
@ -450,13 +589,82 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager):
|
||||||
"type": "entity_not_defined",
|
"type": "entity_not_defined",
|
||||||
"identifier": "sensor.grid_price_1",
|
"identifier": "sensor.grid_price_1",
|
||||||
"value": None,
|
"value": None,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"type": "recorder_untracked",
|
||||||
|
"identifier": "sensor.grid_consumption_1_cost",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "recorder_untracked",
|
||||||
|
"identifier": "sensor.grid_production_1_compensation",
|
||||||
|
"value": None,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"device_consumption": [],
|
"device_consumption": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_validation_grid_auto_cost_entity_errors(
|
||||||
|
hass, mock_energy_manager, mock_get_metadata, mock_is_entity_recorded, caplog
|
||||||
|
):
|
||||||
|
"""Test validating grid when the auto generated cost entity config is incorrect.
|
||||||
|
|
||||||
|
The intention of the test is to make sure the validation does not throw due to the
|
||||||
|
bad config.
|
||||||
|
"""
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.grid_consumption_1",
|
||||||
|
"10.10",
|
||||||
|
{
|
||||||
|
"device_class": "energy",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
"state_class": "total_increasing",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"sensor.grid_production_1",
|
||||||
|
"10.10",
|
||||||
|
{
|
||||||
|
"device_class": "energy",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
"state_class": "total_increasing",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await mock_energy_manager.async_update(
|
||||||
|
{
|
||||||
|
"energy_sources": [
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"flow_from": [
|
||||||
|
{
|
||||||
|
"stat_energy_from": "sensor.grid_consumption_1",
|
||||||
|
"entity_energy_from": None,
|
||||||
|
"entity_energy_price": None,
|
||||||
|
"number_energy_price": 0.20,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flow_to": [
|
||||||
|
{
|
||||||
|
"stat_energy_to": "sensor.grid_production_1",
|
||||||
|
"entity_energy_to": "invalid",
|
||||||
|
"entity_energy_price": None,
|
||||||
|
"number_energy_price": 0.10,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (await validate.async_validate(hass)).as_dict() == {
|
||||||
|
"energy_sources": [[]],
|
||||||
|
"device_consumption": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"state, unit, expected",
|
"state, unit, expected",
|
||||||
(
|
(
|
||||||
|
@ -481,7 +689,7 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_validation_grid_price_errors(
|
async def test_validation_grid_price_errors(
|
||||||
hass, mock_energy_manager, state, unit, expected
|
hass, mock_energy_manager, mock_get_metadata, state, unit, expected
|
||||||
):
|
):
|
||||||
"""Test validating grid with price data that gives errors."""
|
"""Test validating grid with price data that gives errors."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -526,7 +734,9 @@ async def test_validation_grid_price_errors(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded):
|
async def test_validation_gas(
|
||||||
|
hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
|
||||||
|
):
|
||||||
"""Test validating gas with sensors for energy and cost/compensation."""
|
"""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_cost_1"] = False
|
||||||
mock_is_entity_recorded["sensor.gas_compensation_1"] = False
|
mock_is_entity_recorded["sensor.gas_compensation_1"] = False
|
||||||
|
@ -653,7 +863,7 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_gas_no_costs_tracking(
|
async def test_validation_gas_no_costs_tracking(
|
||||||
hass, mock_energy_manager, mock_is_entity_recorded
|
hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
|
||||||
):
|
):
|
||||||
"""Test validating gas with sensors without cost tracking."""
|
"""Test validating gas with sensors without cost tracking."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
|
@ -687,7 +897,7 @@ async def test_validation_gas_no_costs_tracking(
|
||||||
|
|
||||||
|
|
||||||
async def test_validation_grid_no_costs_tracking(
|
async def test_validation_grid_no_costs_tracking(
|
||||||
hass, mock_energy_manager, mock_is_entity_recorded
|
hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata
|
||||||
):
|
):
|
||||||
"""Test validating grid with sensors for energy without cost tracking."""
|
"""Test validating grid with sensors for energy without cost tracking."""
|
||||||
await mock_energy_manager.async_update(
|
await mock_energy_manager.async_update(
|
||||||
|
|
Loading…
Reference in a new issue