mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +00:00
Add energy validation (#54567)
This commit is contained in:
parent
6d0ce814e7
commit
2f77b5025c
@ -4,6 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/energy",
|
"documentation": "https://www.home-assistant.io/integrations/energy",
|
||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"dependencies": ["websocket_api", "history"],
|
"dependencies": ["websocket_api", "history", "recorder"],
|
||||||
"quality_scale": "internal"
|
"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,
|
EnergyPreferencesUpdate,
|
||||||
async_get_manager,
|
async_get_manager,
|
||||||
)
|
)
|
||||||
|
from .validate import async_validate
|
||||||
|
|
||||||
EnergyWebSocketCommandHandler = Callable[
|
EnergyWebSocketCommandHandler = Callable[
|
||||||
[HomeAssistant, websocket_api.ActiveConnection, Dict[str, Any], "EnergyManager"],
|
[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_get_prefs)
|
||||||
websocket_api.async_register_command(hass, ws_save_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_info)
|
||||||
|
websocket_api.async_register_command(hass, ws_validate)
|
||||||
|
|
||||||
|
|
||||||
def _ws_with_manager(
|
def _ws_with_manager(
|
||||||
@ -113,3 +115,18 @@ def ws_info(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get info command."""
|
"""Handle get info command."""
|
||||||
connection.send_result(msg["id"], hass.data[DOMAIN])
|
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
|
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):
|
def run_information(hass, point_in_time: datetime | None = None):
|
||||||
"""Return information about current run.
|
"""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 msg["id"] == 5
|
||||||
assert not msg["success"]
|
assert not msg["success"]
|
||||||
assert msg["error"]["code"] == "invalid_format"
|
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…
x
Reference in New Issue
Block a user