core/homeassistant/components/energy/validate.py

278 lines
8.2 KiB
Python

"""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