Add energy validation (#54567)
This commit is contained in:
parent
6d0ce814e7
commit
2f77b5025c
6 changed files with 765 additions and 1 deletions
|
@ -4,6 +4,6 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/energy",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"iot_class": "calculated",
|
||||
"dependencies": ["websocket_api", "history"],
|
||||
"dependencies": ["websocket_api", "history", "recorder"],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
|
277
homeassistant/components/energy/validate.py
Normal file
277
homeassistant/components/energy/validate.py
Normal file
|
@ -0,0 +1,277 @@
|
|||
"""Validate the energy preferences provide valid data."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components import recorder, sensor
|
||||
from homeassistant.const import (
|
||||
ENERGY_KILO_WATT_HOUR,
|
||||
ENERGY_WATT_HOUR,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||
|
||||
from . import data
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ValidationIssue:
|
||||
"""Error or warning message."""
|
||||
|
||||
type: str
|
||||
identifier: str
|
||||
value: Any | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class EnergyPreferencesValidation:
|
||||
"""Dictionary holding validation information."""
|
||||
|
||||
energy_sources: list[list[ValidationIssue]] = dataclasses.field(
|
||||
default_factory=list
|
||||
)
|
||||
device_consumption: list[list[ValidationIssue]] = dataclasses.field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
"""Return dictionary version."""
|
||||
return dataclasses.asdict(self)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_validate_energy_stat(
|
||||
hass: HomeAssistant, stat_value: str, result: list[ValidationIssue]
|
||||
) -> None:
|
||||
"""Validate a statistic."""
|
||||
has_entity_source = valid_entity_id(stat_value)
|
||||
|
||||
if not has_entity_source:
|
||||
return
|
||||
|
||||
if not recorder.is_entity_recorded(hass, stat_value):
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"recorder_untracked",
|
||||
stat_value,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
state = hass.states.get(stat_value)
|
||||
|
||||
if state is None:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_not_defined",
|
||||
stat_value,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
result.append(ValidationIssue("entity_unavailable", stat_value, state.state))
|
||||
return
|
||||
|
||||
try:
|
||||
current_value: float | None = float(state.state)
|
||||
except ValueError:
|
||||
result.append(
|
||||
ValidationIssue("entity_state_non_numeric", stat_value, state.state)
|
||||
)
|
||||
return
|
||||
|
||||
if current_value is not None and current_value < 0:
|
||||
result.append(
|
||||
ValidationIssue("entity_negative_state", stat_value, current_value)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
state_class = state.attributes.get("state_class")
|
||||
|
||||
if state_class != sensor.STATE_CLASS_TOTAL_INCREASING:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_unexpected_state_class_total_increasing",
|
||||
stat_value,
|
||||
state_class,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_validate_price_entity(
|
||||
hass: HomeAssistant, entity_id: str, result: list[ValidationIssue]
|
||||
) -> None:
|
||||
"""Validate that the price entity is correct."""
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_not_defined",
|
||||
entity_id,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
value: float | None = float(state.state)
|
||||
except ValueError:
|
||||
result.append(
|
||||
ValidationIssue("entity_state_non_numeric", entity_id, state.state)
|
||||
)
|
||||
return
|
||||
|
||||
if value is not None and value < 0:
|
||||
result.append(ValidationIssue("entity_negative_state", entity_id, value))
|
||||
|
||||
unit = state.attributes.get("unit_of_measurement")
|
||||
|
||||
if unit is None or not unit.endswith(
|
||||
(f"/{ENERGY_KILO_WATT_HOUR}", f"/{ENERGY_WATT_HOUR}")
|
||||
):
|
||||
result.append(ValidationIssue("entity_unexpected_unit_price", entity_id, unit))
|
||||
|
||||
|
||||
@callback
|
||||
def _async_validate_cost_stat(
|
||||
hass: HomeAssistant, stat_id: str, result: list[ValidationIssue]
|
||||
) -> None:
|
||||
"""Validate that the cost stat is correct."""
|
||||
has_entity = valid_entity_id(stat_id)
|
||||
|
||||
if not has_entity:
|
||||
return
|
||||
|
||||
if not recorder.is_entity_recorded(hass, stat_id):
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"recorder_untracked",
|
||||
stat_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_validate_cost_entity(
|
||||
hass: HomeAssistant, entity_id: str, result: list[ValidationIssue]
|
||||
) -> None:
|
||||
"""Validate that the cost entity is correct."""
|
||||
if not recorder.is_entity_recorded(hass, entity_id):
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"recorder_untracked",
|
||||
entity_id,
|
||||
)
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_not_defined",
|
||||
entity_id,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
state_class = state.attributes.get("state_class")
|
||||
|
||||
if state_class != sensor.STATE_CLASS_TOTAL_INCREASING:
|
||||
result.append(
|
||||
ValidationIssue(
|
||||
"entity_unexpected_state_class_total_increasing", entity_id, state_class
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
"""Validate the energy configuration."""
|
||||
manager = await data.async_get_manager(hass)
|
||||
|
||||
result = EnergyPreferencesValidation()
|
||||
|
||||
if manager.data is None:
|
||||
return result
|
||||
|
||||
for source in manager.data["energy_sources"]:
|
||||
source_result: list[ValidationIssue] = []
|
||||
result.energy_sources.append(source_result)
|
||||
|
||||
if source["type"] == "grid":
|
||||
for flow in source["flow_from"]:
|
||||
_async_validate_energy_stat(
|
||||
hass, flow["stat_energy_from"], source_result
|
||||
)
|
||||
|
||||
if flow.get("stat_cost") is not None:
|
||||
_async_validate_cost_stat(hass, flow["stat_cost"], source_result)
|
||||
|
||||
elif flow.get("entity_energy_price") is not None:
|
||||
_async_validate_price_entity(
|
||||
hass, flow["entity_energy_price"], source_result
|
||||
)
|
||||
_async_validate_cost_entity(
|
||||
hass,
|
||||
hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_from"]],
|
||||
source_result,
|
||||
)
|
||||
|
||||
for flow in source["flow_to"]:
|
||||
_async_validate_energy_stat(hass, flow["stat_energy_to"], source_result)
|
||||
|
||||
if flow.get("stat_compensation") is not None:
|
||||
_async_validate_cost_stat(
|
||||
hass, flow["stat_compensation"], source_result
|
||||
)
|
||||
|
||||
elif flow.get("entity_energy_price") is not None:
|
||||
_async_validate_price_entity(
|
||||
hass, flow["entity_energy_price"], source_result
|
||||
)
|
||||
_async_validate_cost_entity(
|
||||
hass,
|
||||
hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_to"]],
|
||||
source_result,
|
||||
)
|
||||
|
||||
elif source["type"] == "gas":
|
||||
_async_validate_energy_stat(hass, source["stat_energy_from"], source_result)
|
||||
|
||||
if source.get("stat_cost") is not None:
|
||||
_async_validate_cost_stat(hass, source["stat_cost"], source_result)
|
||||
|
||||
elif source.get("entity_energy_price") is not None:
|
||||
_async_validate_price_entity(
|
||||
hass, source["entity_energy_price"], source_result
|
||||
)
|
||||
_async_validate_cost_entity(
|
||||
hass,
|
||||
hass.data[DOMAIN]["cost_sensors"][source["stat_energy_from"]],
|
||||
source_result,
|
||||
)
|
||||
|
||||
elif source["type"] == "solar":
|
||||
_async_validate_energy_stat(hass, source["stat_energy_from"], 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)
|
||||
|
||||
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)
|
||||
|
||||
return result
|
|
@ -18,6 +18,7 @@ from .data import (
|
|||
EnergyPreferencesUpdate,
|
||||
async_get_manager,
|
||||
)
|
||||
from .validate import async_validate
|
||||
|
||||
EnergyWebSocketCommandHandler = Callable[
|
||||
[HomeAssistant, websocket_api.ActiveConnection, Dict[str, Any], "EnergyManager"],
|
||||
|
@ -35,6 +36,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||
websocket_api.async_register_command(hass, ws_get_prefs)
|
||||
websocket_api.async_register_command(hass, ws_save_prefs)
|
||||
websocket_api.async_register_command(hass, ws_info)
|
||||
websocket_api.async_register_command(hass, ws_validate)
|
||||
|
||||
|
||||
def _ws_with_manager(
|
||||
|
@ -113,3 +115,18 @@ def ws_info(
|
|||
) -> None:
|
||||
"""Handle get info command."""
|
||||
connection.send_result(msg["id"], hass.data[DOMAIN])
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "energy/validate",
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_validate(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict,
|
||||
) -> None:
|
||||
"""Handle validate command."""
|
||||
connection.send_result(msg["id"], (await async_validate(hass)).as_dict())
|
||||
|
|
|
@ -174,6 +174,17 @@ async def async_migration_in_progress(hass: HomeAssistant) -> bool:
|
|||
return hass.data[DATA_INSTANCE].migration_in_progress
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Check if an entity is being recorded.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
if DATA_INSTANCE not in hass.data:
|
||||
return False
|
||||
return hass.data[DATA_INSTANCE].entity_filter(entity_id)
|
||||
|
||||
|
||||
def run_information(hass, point_in_time: datetime | None = None):
|
||||
"""Return information about current run.
|
||||
|
||||
|
|
443
tests/components/energy/test_validate.py
Normal file
443
tests/components/energy/test_validate.py
Normal file
|
@ -0,0 +1,443 @@
|
|||
"""Test that validation works."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.energy import async_get_manager, validate
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_init_recorder_component
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_is_entity_recorded():
|
||||
"""Mock recorder.is_entity_recorded."""
|
||||
mocks = {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.recorder.is_entity_recorded",
|
||||
side_effect=lambda hass, entity_id: mocks.get(entity_id, True),
|
||||
):
|
||||
yield mocks
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def mock_energy_manager(hass):
|
||||
"""Set up energy."""
|
||||
await async_init_recorder_component(hass)
|
||||
assert await async_setup_component(hass, "energy", {"energy": {}})
|
||||
manager = await async_get_manager(hass)
|
||||
manager.data = manager.default_preferences()
|
||||
return manager
|
||||
|
||||
|
||||
async def test_validation_empty_config(hass):
|
||||
"""Test validating an empty config."""
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation(hass, mock_energy_manager):
|
||||
"""Test validating success."""
|
||||
for key in ("device_cons", "battery_import", "battery_export", "solar_production"):
|
||||
hass.states.async_set(
|
||||
f"sensor.{key}",
|
||||
"123",
|
||||
{"unit_of_measurement": "kWh", "state_class": "total_increasing"},
|
||||
)
|
||||
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_import",
|
||||
"stat_energy_to": "sensor.battery_export",
|
||||
},
|
||||
{"type": "solar", "stat_energy_from": "sensor.solar_production"},
|
||||
],
|
||||
"device_consumption": [{"stat_consumption": "sensor.device_cons"}],
|
||||
}
|
||||
)
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [[], []],
|
||||
"device_consumption": [[]],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_device_consumption_entity_missing(hass, mock_energy_manager):
|
||||
"""Test validating missing stat for device."""
|
||||
await mock_energy_manager.async_update(
|
||||
{"device_consumption": [{"stat_consumption": "sensor.not_exist"}]}
|
||||
)
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [
|
||||
[
|
||||
{
|
||||
"type": "entity_not_defined",
|
||||
"identifier": "sensor.not_exist",
|
||||
"value": None,
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_device_consumption_entity_unavailable(
|
||||
hass, mock_energy_manager
|
||||
):
|
||||
"""Test validating missing stat for device."""
|
||||
await mock_energy_manager.async_update(
|
||||
{"device_consumption": [{"stat_consumption": "sensor.unavailable"}]}
|
||||
)
|
||||
hass.states.async_set("sensor.unavailable", "unavailable", {})
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [
|
||||
[
|
||||
{
|
||||
"type": "entity_unavailable",
|
||||
"identifier": "sensor.unavailable",
|
||||
"value": "unavailable",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_device_consumption_entity_non_numeric(
|
||||
hass, mock_energy_manager
|
||||
):
|
||||
"""Test validating missing stat for device."""
|
||||
await mock_energy_manager.async_update(
|
||||
{"device_consumption": [{"stat_consumption": "sensor.non_numeric"}]}
|
||||
)
|
||||
hass.states.async_set("sensor.non_numeric", "123,123.10")
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [
|
||||
[
|
||||
{
|
||||
"type": "entity_state_non_numeric",
|
||||
"identifier": "sensor.non_numeric",
|
||||
"value": "123,123.10",
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_device_consumption_entity_unexpected_unit(
|
||||
hass, mock_energy_manager
|
||||
):
|
||||
"""Test validating missing stat for device."""
|
||||
await mock_energy_manager.async_update(
|
||||
{"device_consumption": [{"stat_consumption": "sensor.unexpected_unit"}]}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.unexpected_unit",
|
||||
"10.10",
|
||||
{"unit_of_measurement": "beers", "state_class": "total_increasing"},
|
||||
)
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [
|
||||
[
|
||||
{
|
||||
"type": "entity_unexpected_unit_energy",
|
||||
"identifier": "sensor.unexpected_unit",
|
||||
"value": "beers",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_device_consumption_recorder_not_tracked(
|
||||
hass, mock_energy_manager, mock_is_entity_recorded
|
||||
):
|
||||
"""Test validating device based on untracked entity."""
|
||||
mock_is_entity_recorded["sensor.not_recorded"] = False
|
||||
await mock_energy_manager.async_update(
|
||||
{"device_consumption": [{"stat_consumption": "sensor.not_recorded"}]}
|
||||
)
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [
|
||||
[
|
||||
{
|
||||
"type": "recorder_untracked",
|
||||
"identifier": "sensor.not_recorded",
|
||||
"value": None,
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_solar(hass, mock_energy_manager):
|
||||
"""Test validating missing stat for device."""
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{"type": "solar", "stat_energy_from": "sensor.solar_production"}
|
||||
]
|
||||
}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.solar_production",
|
||||
"10.10",
|
||||
{"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.solar_production",
|
||||
"value": "beers",
|
||||
}
|
||||
]
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_battery(hass, mock_energy_manager):
|
||||
"""Test validating missing stat for device."""
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_import",
|
||||
"stat_energy_to": "sensor.battery_export",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.battery_import",
|
||||
"10.10",
|
||||
{"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"},
|
||||
)
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [
|
||||
[
|
||||
{
|
||||
"type": "entity_unexpected_unit_energy",
|
||||
"identifier": "sensor.battery_import",
|
||||
"value": "beers",
|
||||
},
|
||||
{
|
||||
"type": "entity_unexpected_unit_energy",
|
||||
"identifier": "sensor.battery_export",
|
||||
"value": "beers",
|
||||
},
|
||||
]
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorded):
|
||||
"""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_compensation_1"] = False
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "grid",
|
||||
"flow_from": [
|
||||
{
|
||||
"stat_energy_from": "sensor.grid_consumption_1",
|
||||
"stat_cost": "sensor.grid_cost_1",
|
||||
}
|
||||
],
|
||||
"flow_to": [
|
||||
{
|
||||
"stat_energy_to": "sensor.grid_production_1",
|
||||
"stat_compensation": "sensor.grid_compensation_1",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.grid_consumption_1",
|
||||
"10.10",
|
||||
{"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"},
|
||||
)
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [
|
||||
[
|
||||
{
|
||||
"type": "entity_unexpected_unit_energy",
|
||||
"identifier": "sensor.grid_consumption_1",
|
||||
"value": "beers",
|
||||
},
|
||||
{
|
||||
"type": "recorder_untracked",
|
||||
"identifier": "sensor.grid_cost_1",
|
||||
"value": None,
|
||||
},
|
||||
{
|
||||
"type": "entity_unexpected_unit_energy",
|
||||
"identifier": "sensor.grid_production_1",
|
||||
"value": "beers",
|
||||
},
|
||||
{
|
||||
"type": "recorder_untracked",
|
||||
"identifier": "sensor.grid_compensation_1",
|
||||
"value": None,
|
||||
},
|
||||
]
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
|
||||
async def test_validation_grid_price_not_exist(hass, mock_energy_manager):
|
||||
"""Test validating grid with price entity that does not exist."""
|
||||
hass.states.async_set(
|
||||
"sensor.grid_consumption_1",
|
||||
"10.10",
|
||||
{"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"},
|
||||
)
|
||||
await mock_energy_manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "grid",
|
||||
"flow_from": [
|
||||
{
|
||||
"stat_energy_from": "sensor.grid_consumption_1",
|
||||
"entity_energy_from": "sensor.grid_consumption_1",
|
||||
"entity_energy_price": "sensor.grid_price_1",
|
||||
}
|
||||
],
|
||||
"flow_to": [
|
||||
{
|
||||
"stat_energy_to": "sensor.grid_production_1",
|
||||
"entity_energy_to": "sensor.grid_production_1",
|
||||
"number_energy_price": 0.10,
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [
|
||||
[
|
||||
{
|
||||
"type": "entity_not_defined",
|
||||
"identifier": "sensor.grid_price_1",
|
||||
"value": None,
|
||||
}
|
||||
]
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"state, unit, expected",
|
||||
(
|
||||
(
|
||||
"123,123.12",
|
||||
"$/kWh",
|
||||
{
|
||||
"type": "entity_state_non_numeric",
|
||||
"identifier": "sensor.grid_price_1",
|
||||
"value": "123,123.12",
|
||||
},
|
||||
),
|
||||
(
|
||||
"-100",
|
||||
"$/kWh",
|
||||
{
|
||||
"type": "entity_negative_state",
|
||||
"identifier": "sensor.grid_price_1",
|
||||
"value": -100.0,
|
||||
},
|
||||
),
|
||||
(
|
||||
"123",
|
||||
"$/Ws",
|
||||
{
|
||||
"type": "entity_unexpected_unit_price",
|
||||
"identifier": "sensor.grid_price_1",
|
||||
"value": "$/Ws",
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_validation_grid_price_errors(
|
||||
hass, mock_energy_manager, state, unit, expected
|
||||
):
|
||||
"""Test validating grid with price data that gives errors."""
|
||||
hass.states.async_set(
|
||||
"sensor.grid_consumption_1",
|
||||
"10.10",
|
||||
{"unit_of_measurement": "kWh", "state_class": "total_increasing"},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"sensor.grid_price_1",
|
||||
state,
|
||||
{"unit_of_measurement": unit, "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": "sensor.grid_consumption_1",
|
||||
"entity_energy_price": "sensor.grid_price_1",
|
||||
}
|
||||
],
|
||||
"flow_to": [],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (await validate.async_validate(hass)).as_dict() == {
|
||||
"energy_sources": [
|
||||
[expected],
|
||||
],
|
||||
"device_consumption": [],
|
||||
}
|
|
@ -216,3 +216,19 @@ async def test_handle_duplicate_from_stat(hass, hass_ws_client) -> None:
|
|||
assert msg["id"] == 5
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "invalid_format"
|
||||
|
||||
|
||||
async def test_validate(hass, hass_ws_client) -> None:
|
||||
"""Test we can validate the preferences."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 5, "type": "energy/validate"})
|
||||
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["id"] == 5
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {
|
||||
"energy_sources": [],
|
||||
"device_consumption": [],
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue