mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Make statistics validation create issue registry issues (#122595)
* Make statistics validation create issue registry issues * Disable creating issue about outdated MariaDB version in tests * Use call_soon_threadsafe instead of run_callback_threadsafe * Update tests * Fix flapping test * Disable creating issue about outdated SQLite version in tests * Implement agreed changes * Add translation strings for issue titles * Update test
This commit is contained in:
parent
d6e34e0984
commit
771575cfc5
@ -62,13 +62,15 @@ LAST_REPORTED_SCHEMA_VERSION = 43
|
||||
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28
|
||||
|
||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics"
|
||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS = "validate_statistics"
|
||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS = "list_statistic_ids"
|
||||
INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES = "update_statistics_issues"
|
||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS = "validate_statistics"
|
||||
|
||||
INTEGRATION_PLATFORM_METHODS = {
|
||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS,
|
||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS,
|
||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS,
|
||||
INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES,
|
||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS,
|
||||
}
|
||||
|
||||
|
||||
|
@ -52,6 +52,7 @@ from .const import (
|
||||
EVENT_RECORDER_HOURLY_STATISTICS_GENERATED,
|
||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS,
|
||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS,
|
||||
INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES,
|
||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS,
|
||||
SupportedDialect,
|
||||
)
|
||||
@ -586,6 +587,17 @@ def _compile_statistics(
|
||||
):
|
||||
new_short_term_stats.append(new_stat)
|
||||
|
||||
if start.minute == 50:
|
||||
# Once every hour, update issues
|
||||
for platform in instance.hass.data[DOMAIN].recorder_platforms.values():
|
||||
if not (
|
||||
platform_update_issues := getattr(
|
||||
platform, INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES, None
|
||||
)
|
||||
):
|
||||
continue
|
||||
platform_update_issues(instance.hass, session)
|
||||
|
||||
if start.minute == 55:
|
||||
# A full hour is ready, summarize it
|
||||
_compile_hourly_statistics(session, start)
|
||||
@ -2212,6 +2224,16 @@ def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]
|
||||
return platform_validation
|
||||
|
||||
|
||||
def update_statistics_issues(hass: HomeAssistant) -> None:
|
||||
"""Update statistics issues."""
|
||||
with session_scope(hass=hass, read_only=True) as session:
|
||||
for platform in hass.data[DOMAIN].recorder_platforms.values():
|
||||
if platform_update_statistics_issues := getattr(
|
||||
platform, INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES, None
|
||||
):
|
||||
platform_update_statistics_issues(hass, session)
|
||||
|
||||
|
||||
def _statistics_exists(
|
||||
session: Session,
|
||||
table: type[StatisticsBase],
|
||||
|
@ -43,6 +43,7 @@ from .statistics import (
|
||||
list_statistic_ids,
|
||||
statistic_during_period,
|
||||
statistics_during_period,
|
||||
update_statistics_issues,
|
||||
validate_statistics,
|
||||
)
|
||||
from .util import PERIOD_SCHEMA, get_instance, resolve_period
|
||||
@ -80,6 +81,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
websocket_api.async_register_command(hass, ws_get_statistics_metadata)
|
||||
websocket_api.async_register_command(hass, ws_list_statistic_ids)
|
||||
websocket_api.async_register_command(hass, ws_import_statistics)
|
||||
websocket_api.async_register_command(hass, ws_update_statistics_issues)
|
||||
websocket_api.async_register_command(hass, ws_update_statistics_metadata)
|
||||
websocket_api.async_register_command(hass, ws_validate_statistics)
|
||||
|
||||
@ -292,6 +294,24 @@ async def ws_validate_statistics(
|
||||
connection.send_result(msg["id"], statistic_ids)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "recorder/update_statistics_issues",
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
async def ws_update_statistics_issues(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Update statistics issues."""
|
||||
instance = get_instance(hass)
|
||||
await instance.async_add_executor_job(
|
||||
update_statistics_issues,
|
||||
hass,
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Iterable
|
||||
import datetime
|
||||
from functools import partial
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
@ -30,8 +31,9 @@ from homeassistant.const import (
|
||||
UnitOfSoundPressure,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.entity import entity_sources
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
@ -672,6 +674,113 @@ def list_statistic_ids(
|
||||
return result
|
||||
|
||||
|
||||
@callback
|
||||
def _update_issues(
|
||||
report_issue: Callable[[str, str, dict[str, Any]], None],
|
||||
clear_issue: Callable[[str, str], None],
|
||||
sensor_states: list[State],
|
||||
metadatas: dict[str, tuple[int, StatisticMetaData]],
|
||||
) -> None:
|
||||
"""Update repair issues."""
|
||||
for state in sensor_states:
|
||||
entity_id = state.entity_id
|
||||
state_class = try_parse_enum(
|
||||
SensorStateClass, state.attributes.get(ATTR_STATE_CLASS)
|
||||
)
|
||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if metadata := metadatas.get(entity_id):
|
||||
if state_class is None:
|
||||
# Sensor no longer has a valid state class
|
||||
report_issue(
|
||||
"unsupported_state_class",
|
||||
entity_id,
|
||||
{
|
||||
"statistic_id": entity_id,
|
||||
"state_class": state_class,
|
||||
},
|
||||
)
|
||||
else:
|
||||
clear_issue("unsupported_state_class", entity_id)
|
||||
|
||||
metadata_unit = metadata[1]["unit_of_measurement"]
|
||||
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
|
||||
if not converter:
|
||||
if not _equivalent_units({state_unit, metadata_unit}):
|
||||
# The unit has changed, and it's not possible to convert
|
||||
report_issue(
|
||||
"units_changed",
|
||||
entity_id,
|
||||
{
|
||||
"statistic_id": entity_id,
|
||||
"state_unit": state_unit,
|
||||
"metadata_unit": metadata_unit,
|
||||
"supported_unit": metadata_unit,
|
||||
},
|
||||
)
|
||||
else:
|
||||
clear_issue("units_changed", entity_id)
|
||||
elif state_unit not in converter.VALID_UNITS:
|
||||
# The state unit can't be converted to the unit in metadata
|
||||
valid_units = (unit or "<None>" for unit in converter.VALID_UNITS)
|
||||
valid_units_str = ", ".join(sorted(valid_units))
|
||||
report_issue(
|
||||
"units_changed",
|
||||
entity_id,
|
||||
{
|
||||
"statistic_id": entity_id,
|
||||
"state_unit": state_unit,
|
||||
"metadata_unit": metadata_unit,
|
||||
"supported_unit": valid_units_str,
|
||||
},
|
||||
)
|
||||
else:
|
||||
clear_issue("units_changed", entity_id)
|
||||
|
||||
|
||||
def update_statistics_issues(
|
||||
hass: HomeAssistant,
|
||||
session: Session,
|
||||
) -> None:
|
||||
"""Validate statistics."""
|
||||
instance = get_instance(hass)
|
||||
sensor_states = hass.states.all(DOMAIN)
|
||||
metadatas = statistics.get_metadata_with_session(
|
||||
instance, session, statistic_source=RECORDER_DOMAIN
|
||||
)
|
||||
|
||||
def create_issue_registry_issue(
|
||||
issue_type: str, statistic_id: str, data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Create an issue registry issue."""
|
||||
hass.loop.call_soon_threadsafe(
|
||||
partial(
|
||||
ir.async_create_issue,
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{issue_type}_{statistic_id}",
|
||||
data=data | {"issue_type": issue_type},
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=issue_type,
|
||||
translation_placeholders=data,
|
||||
)
|
||||
)
|
||||
|
||||
def delete_issue_registry_issue(issue_type: str, statistic_id: str) -> None:
|
||||
"""Delete an issue registry issue."""
|
||||
hass.loop.call_soon_threadsafe(
|
||||
ir.async_delete_issue, hass, DOMAIN, f"{issue_type}_{statistic_id}"
|
||||
)
|
||||
|
||||
_update_issues(
|
||||
create_issue_registry_issue,
|
||||
delete_issue_registry_issue,
|
||||
sensor_states,
|
||||
metadatas,
|
||||
)
|
||||
|
||||
|
||||
def validate_statistics(
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, list[statistics.ValidationIssue]]:
|
||||
@ -685,14 +794,28 @@ def validate_statistics(
|
||||
instance = get_instance(hass)
|
||||
entity_filter = instance.entity_filter
|
||||
|
||||
def create_statistic_validation_issue(
|
||||
issue_type: str, statistic_id: str, data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Create a statistic validation issue."""
|
||||
validation_result[statistic_id].append(
|
||||
statistics.ValidationIssue(issue_type, data)
|
||||
)
|
||||
|
||||
_update_issues(
|
||||
create_statistic_validation_issue,
|
||||
lambda issue_type, statistic_id: None,
|
||||
sensor_states,
|
||||
metadatas,
|
||||
)
|
||||
|
||||
for state in sensor_states:
|
||||
entity_id = state.entity_id
|
||||
state_class = try_parse_enum(
|
||||
SensorStateClass, state.attributes.get(ATTR_STATE_CLASS)
|
||||
)
|
||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if metadata := metadatas.get(entity_id):
|
||||
if entity_id in metadatas:
|
||||
if entity_filter and not entity_filter(state.entity_id):
|
||||
# Sensor was previously recorded, but no longer is
|
||||
validation_result[entity_id].append(
|
||||
@ -701,47 +824,6 @@ def validate_statistics(
|
||||
{"statistic_id": entity_id},
|
||||
)
|
||||
)
|
||||
|
||||
if state_class is None:
|
||||
# Sensor no longer has a valid state class
|
||||
validation_result[entity_id].append(
|
||||
statistics.ValidationIssue(
|
||||
"unsupported_state_class",
|
||||
{"statistic_id": entity_id, "state_class": state_class},
|
||||
)
|
||||
)
|
||||
|
||||
metadata_unit = metadata[1]["unit_of_measurement"]
|
||||
converter = statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER.get(metadata_unit)
|
||||
if not converter:
|
||||
if not _equivalent_units({state_unit, metadata_unit}):
|
||||
# The unit has changed, and it's not possible to convert
|
||||
validation_result[entity_id].append(
|
||||
statistics.ValidationIssue(
|
||||
"units_changed",
|
||||
{
|
||||
"statistic_id": entity_id,
|
||||
"state_unit": state_unit,
|
||||
"metadata_unit": metadata_unit,
|
||||
"supported_unit": metadata_unit,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif state_unit not in converter.VALID_UNITS:
|
||||
# The state unit can't be converted to the unit in metadata
|
||||
valid_units = (unit or "<None>" for unit in converter.VALID_UNITS)
|
||||
valid_units_str = ", ".join(sorted(valid_units))
|
||||
validation_result[entity_id].append(
|
||||
statistics.ValidationIssue(
|
||||
"units_changed",
|
||||
{
|
||||
"statistic_id": entity_id,
|
||||
"state_unit": state_unit,
|
||||
"metadata_unit": metadata_unit,
|
||||
"supported_unit": valid_units_str,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif state_class is not None:
|
||||
if entity_filter and not entity_filter(state.entity_id):
|
||||
# Sensor is not recorded
|
||||
|
@ -287,5 +287,15 @@
|
||||
"wind_speed": {
|
||||
"name": "Wind speed"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"units_changed": {
|
||||
"title": "The unit of {statistic_id} has changed",
|
||||
"description": ""
|
||||
},
|
||||
"unsupported_state_class": {
|
||||
"title": "The state class of {statistic_id} is not supported",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2512,6 +2512,7 @@ async def test_recorder_platform_with_statistics(
|
||||
recorder_platform = Mock(
|
||||
compile_statistics=Mock(wraps=_mock_compile_statistics),
|
||||
list_statistic_ids=Mock(wraps=_mock_list_statistic_ids),
|
||||
update_statistics_issues=Mock(),
|
||||
validate_statistics=Mock(wraps=_mock_validate_statistics),
|
||||
)
|
||||
|
||||
@ -2523,16 +2524,20 @@ async def test_recorder_platform_with_statistics(
|
||||
|
||||
recorder_platform.compile_statistics.assert_not_called()
|
||||
recorder_platform.list_statistic_ids.assert_not_called()
|
||||
recorder_platform.update_statistics_issues.assert_not_called()
|
||||
recorder_platform.validate_statistics.assert_not_called()
|
||||
|
||||
# Test compile statistics
|
||||
zero = get_start_time(dt_util.utcnow())
|
||||
# Test compile statistics + update statistics issues
|
||||
# Issues are updated hourly when minutes = 50, trigger one hour later to make
|
||||
# sure statistics is not suppressed by an existing row in StatisticsRuns
|
||||
zero = get_start_time(dt_util.utcnow()).replace(minute=50) + timedelta(hours=1)
|
||||
do_adhoc_statistics(hass, start=zero)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
recorder_platform.compile_statistics.assert_called_once_with(
|
||||
hass, ANY, zero, zero + timedelta(minutes=5)
|
||||
)
|
||||
recorder_platform.update_statistics_issues.assert_called_once_with(hass, ANY)
|
||||
recorder_platform.list_statistic_ids.assert_not_called()
|
||||
recorder_platform.validate_statistics.assert_not_called()
|
||||
|
||||
@ -2542,6 +2547,7 @@ async def test_recorder_platform_with_statistics(
|
||||
recorder_platform.list_statistic_ids.assert_called_once_with(
|
||||
hass, statistic_ids=None, statistic_type=None
|
||||
)
|
||||
recorder_platform.update_statistics_issues.assert_called_once()
|
||||
recorder_platform.validate_statistics.assert_not_called()
|
||||
|
||||
# Test validate statistics
|
||||
@ -2551,6 +2557,7 @@ async def test_recorder_platform_with_statistics(
|
||||
)
|
||||
recorder_platform.compile_statistics.assert_called_once()
|
||||
recorder_platform.list_statistic_ids.assert_called_once()
|
||||
recorder_platform.update_statistics_issues.assert_called_once()
|
||||
recorder_platform.validate_statistics.assert_called_once_with(hass)
|
||||
|
||||
|
||||
@ -2575,6 +2582,7 @@ async def test_recorder_platform_without_statistics(
|
||||
[
|
||||
("compile_statistics",),
|
||||
("list_statistic_ids",),
|
||||
("update_statistics_issues",),
|
||||
("validate_statistics",),
|
||||
],
|
||||
)
|
||||
@ -2601,6 +2609,7 @@ async def test_recorder_platform_with_partial_statistics_support(
|
||||
mock_impl = {
|
||||
"compile_statistics": _mock_compile_statistics,
|
||||
"list_statistic_ids": _mock_list_statistic_ids,
|
||||
"update_statistics_issues": None,
|
||||
"validate_statistics": _mock_validate_statistics,
|
||||
}
|
||||
|
||||
@ -2620,8 +2629,10 @@ async def test_recorder_platform_with_partial_statistics_support(
|
||||
for meth in supported_methods:
|
||||
getattr(recorder_platform, meth).assert_not_called()
|
||||
|
||||
# Test compile statistics
|
||||
zero = get_start_time(dt_util.utcnow())
|
||||
# Test compile statistics + update statistics issues
|
||||
# Issues are updated hourly when minutes = 50, trigger one hour later to make
|
||||
# sure statistics is not suppressed by an existing row in StatisticsRuns
|
||||
zero = get_start_time(dt_util.utcnow()).replace(minute=50) + timedelta(hours=1)
|
||||
do_adhoc_statistics(hass, start=zero)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
|
@ -1984,6 +1984,18 @@ async def test_validate_statistics(
|
||||
await assert_validation_result(client, {})
|
||||
|
||||
|
||||
async def test_update_statistics_issues(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test update_statistics_issues can be called."""
|
||||
|
||||
client = await hass_ws_client()
|
||||
await client.send_json_auto_id({"type": "recorder/update_statistics_issues"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] is None
|
||||
|
||||
|
||||
async def test_clear_statistics(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""The tests for sensor recorder platform."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta
|
||||
import math
|
||||
from statistics import mean
|
||||
from typing import Any, Literal
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
@ -37,6 +38,7 @@ from homeassistant.components.recorder.util import get_instance, session_scope
|
||||
from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
@ -110,6 +112,24 @@ def setup_recorder(recorder_mock: Recorder) -> Recorder:
|
||||
"""Set up recorder."""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_mariadb_issue() -> None:
|
||||
"""Disable creating issue about outdated MariaDB version."""
|
||||
with patch(
|
||||
"homeassistant.components.recorder.util._async_create_mariadb_range_index_regression_issue"
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_sqlite_issue() -> None:
|
||||
"""Disable creating issue about outdated SQLite version."""
|
||||
with patch(
|
||||
"homeassistant.components.recorder.util._async_create_issue_deprecated_version"
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def async_list_statistic_ids(
|
||||
hass: HomeAssistant,
|
||||
statistic_ids: set[str] | None = None,
|
||||
@ -137,15 +157,61 @@ async def assert_statistic_ids(
|
||||
)
|
||||
|
||||
|
||||
def assert_issues(
|
||||
hass: HomeAssistant,
|
||||
expected_issues: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
"""Assert statistics issues."""
|
||||
issue_registry = ir.async_get(hass)
|
||||
assert len(issue_registry.issues) == len(expected_issues)
|
||||
for issue_id, expected_issue_data in expected_issues.items():
|
||||
expected_translation_placeholders = dict(expected_issue_data)
|
||||
expected_translation_placeholders.pop("issue_type")
|
||||
expected_issue = ir.IssueEntry(
|
||||
active=True,
|
||||
breaks_in_ha_version=None,
|
||||
created=ANY,
|
||||
data=expected_issue_data,
|
||||
dismissed_version=None,
|
||||
domain=DOMAIN,
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
issue_domain=None,
|
||||
issue_id=issue_id,
|
||||
learn_more_url=None,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=expected_issue_data["issue_type"],
|
||||
translation_placeholders=expected_translation_placeholders,
|
||||
)
|
||||
assert (DOMAIN, issue_id) in issue_registry.issues
|
||||
assert issue_registry.issues[(DOMAIN, issue_id)] == expected_issue
|
||||
|
||||
|
||||
async def assert_validation_result(
|
||||
hass: HomeAssistant,
|
||||
client: MockHAClientWebSocket,
|
||||
expected_result: dict[str, list[dict[str, Any]]],
|
||||
expected_validation_result: dict[str, list[dict[str, Any]]],
|
||||
expected_issues: Iterable[str],
|
||||
) -> None:
|
||||
"""Assert statistics validation result."""
|
||||
await client.send_json_auto_id({"type": "recorder/validate_statistics"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"] == expected_result
|
||||
assert response["result"] == expected_validation_result
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check we get corresponding issues
|
||||
await client.send_json_auto_id({"type": "recorder/update_statistics_issues"})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
expected_issue_registry_issues = {
|
||||
f"{issue['type']}_{statistic_id}": issue["data"] | {"issue_type": issue["type"]}
|
||||
for statistic_id, issues in expected_validation_result.items()
|
||||
for issue in issues
|
||||
if issue["type"] in expected_issues
|
||||
}
|
||||
|
||||
assert_issues(hass, expected_issue_registry_issues)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4219,7 +4285,7 @@ async def test_validate_unit_change_convertible(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, unit in state matching device class - empty response
|
||||
hass.states.async_set(
|
||||
@ -4229,7 +4295,7 @@ async def test_validate_unit_change_convertible(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, unit in state not matching device class - empty response
|
||||
hass.states.async_set(
|
||||
@ -4239,7 +4305,7 @@ async def test_validate_unit_change_convertible(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Statistics has run, incompatible unit - expect error
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4264,7 +4330,7 @@ async def test_validate_unit_change_convertible(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {"units_changed"})
|
||||
|
||||
# Valid state - empty response
|
||||
hass.states.async_set(
|
||||
@ -4274,12 +4340,12 @@ async def test_validate_unit_change_convertible(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state, statistic runs again - empty response
|
||||
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state in compatible unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4289,12 +4355,12 @@ async def test_validate_unit_change_convertible(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state, statistic runs again - empty response
|
||||
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Remove the state - expect error about missing state
|
||||
hass.states.async_remove("sensor.test")
|
||||
@ -4306,7 +4372,7 @@ async def test_validate_unit_change_convertible(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4333,7 +4399,7 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, no device class - empty response
|
||||
initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"}
|
||||
@ -4341,7 +4407,7 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||
"sensor.test", 10, attributes=initial_attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Statistics has run, device class set not matching unit - empty response
|
||||
do_adhoc_statistics(hass, start=now)
|
||||
@ -4353,7 +4419,7 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4418,7 +4484,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, sensor state set - empty response
|
||||
hass.states.async_set(
|
||||
@ -4428,7 +4494,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, sensor state set to an incompatible unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4438,7 +4504,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Statistics has run, incompatible unit - expect error
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4463,7 +4529,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {"units_changed"})
|
||||
|
||||
# Valid state - empty response
|
||||
hass.states.async_set(
|
||||
@ -4473,12 +4539,12 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state, statistic runs again - empty response
|
||||
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state in compatible unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4488,12 +4554,12 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state, statistic runs again - empty response
|
||||
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Remove the state - expect error about missing state
|
||||
hass.states.async_remove("sensor.test")
|
||||
@ -4505,7 +4571,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4530,19 +4596,19 @@ async def test_validate_statistics_unsupported_state_class(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, valid state - empty response
|
||||
hass.states.async_set(
|
||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Statistics has run, empty response
|
||||
do_adhoc_statistics(hass, start=now)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# State update with invalid state class, expect error
|
||||
_attributes = dict(attributes)
|
||||
@ -4562,7 +4628,7 @@ async def test_validate_statistics_unsupported_state_class(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {"unsupported_state_class"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4587,19 +4653,19 @@ async def test_validate_statistics_sensor_no_longer_recorded(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, valid state - empty response
|
||||
hass.states.async_set(
|
||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Statistics has run, empty response
|
||||
do_adhoc_statistics(hass, start=now)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Sensor no longer recorded, expect error
|
||||
expected = {
|
||||
@ -4616,7 +4682,7 @@ async def test_validate_statistics_sensor_no_longer_recorded(
|
||||
"entity_filter",
|
||||
return_value=False,
|
||||
):
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4641,7 +4707,7 @@ async def test_validate_statistics_sensor_not_recorded(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Sensor not recorded, expect error
|
||||
expected = {
|
||||
@ -4662,12 +4728,12 @@ async def test_validate_statistics_sensor_not_recorded(
|
||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
# Statistics has run, expect same error
|
||||
do_adhoc_statistics(hass, start=now)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4692,19 +4758,19 @@ async def test_validate_statistics_sensor_removed(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, valid state - empty response
|
||||
hass.states.async_set(
|
||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Statistics has run, empty response
|
||||
do_adhoc_statistics(hass, start=now)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Sensor removed, expect error
|
||||
hass.states.async_remove("sensor.test")
|
||||
@ -4716,7 +4782,7 @@ async def test_validate_statistics_sensor_removed(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4741,7 +4807,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, original unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4750,7 +4816,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
attributes={**attributes, "unit_of_measurement": unit1},
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, changed unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4759,7 +4825,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
attributes={**attributes, "unit_of_measurement": unit2},
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Run statistics, no statistics will be generated because of conflicting units
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4774,7 +4840,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
attributes={**attributes, "unit_of_measurement": unit1},
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Run statistics one hour later, only the state with unit1 will be considered
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4783,7 +4849,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
await assert_statistic_ids(
|
||||
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Change unit - expect error
|
||||
hass.states.async_set(
|
||||
@ -4806,7 +4872,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {"units_changed"})
|
||||
|
||||
# Original unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4816,13 +4882,13 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Valid state, statistic runs again - empty response
|
||||
await async_recorder_block_till_done(hass)
|
||||
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
||||
await async_recorder_block_till_done(hass)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Remove the state - expect error
|
||||
hass.states.async_remove("sensor.test")
|
||||
@ -4834,7 +4900,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4864,7 +4930,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, original unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4873,7 +4939,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||
attributes={**attributes, "unit_of_measurement": unit1},
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Run statistics
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4890,7 +4956,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||
attributes={**attributes, "unit_of_measurement": unit2},
|
||||
timestamp=now.timestamp() + 1,
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Run statistics one hour later, metadata will be updated
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4899,7 +4965,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||
await assert_statistic_ids(
|
||||
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit2}]
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -4928,7 +4994,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2(
|
||||
client = await hass_ws_client()
|
||||
|
||||
# No statistics, no state - empty response
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# No statistics, original unit - empty response
|
||||
hass.states.async_set(
|
||||
@ -4937,7 +5003,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2(
|
||||
attributes={**attributes, "unit_of_measurement": unit1},
|
||||
timestamp=now.timestamp(),
|
||||
)
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
# Run statistics
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4967,7 +5033,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2(
|
||||
}
|
||||
],
|
||||
}
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {"units_changed"})
|
||||
|
||||
# Run statistics one hour later, metadata will not be updated
|
||||
await async_recorder_block_till_done(hass)
|
||||
@ -4976,7 +5042,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2(
|
||||
await assert_statistic_ids(
|
||||
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
|
||||
)
|
||||
await assert_validation_result(client, expected)
|
||||
await assert_validation_result(hass, client, expected, {"units_changed"})
|
||||
|
||||
|
||||
async def test_validate_statistics_other_domain(
|
||||
@ -5009,7 +5075,68 @@ async def test_validate_statistics_other_domain(
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
# We should not get complains about the missing number entity
|
||||
await assert_validation_result(client, {})
|
||||
await assert_validation_result(hass, client, {}, {})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("units", "attributes", "unit"),
|
||||
[
|
||||
(US_CUSTOMARY_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"),
|
||||
],
|
||||
)
|
||||
async def test_update_statistics_issues(
|
||||
hass: HomeAssistant,
|
||||
units,
|
||||
attributes,
|
||||
unit,
|
||||
) -> None:
|
||||
"""Test update_statistics_issues."""
|
||||
|
||||
async def one_hour_stats(start: datetime) -> datetime:
|
||||
"""Generate 5-minute statistics for one hour."""
|
||||
for _ in range(12):
|
||||
do_adhoc_statistics(hass, start=start)
|
||||
await async_wait_recording_done(hass)
|
||||
start += timedelta(minutes=5)
|
||||
return start
|
||||
|
||||
now = get_start_time(dt_util.utcnow())
|
||||
|
||||
hass.config.units = units
|
||||
await async_setup_component(hass, "sensor", {})
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
# No statistics, no state - no issues
|
||||
now = await one_hour_stats(now)
|
||||
assert_issues(hass, {})
|
||||
|
||||
# Statistics, valid state - no issues
|
||||
hass.states.async_set(
|
||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
now = await one_hour_stats(now)
|
||||
assert_issues(hass, {})
|
||||
|
||||
# State update with invalid state class, statistics did not run again
|
||||
_attributes = dict(attributes)
|
||||
_attributes.pop("state_class")
|
||||
hass.states.async_set(
|
||||
"sensor.test", 12, attributes=_attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert_issues(hass, {})
|
||||
|
||||
# Let statistics run for one hour, expect issue
|
||||
now = await one_hour_stats(now)
|
||||
expected = {
|
||||
"unsupported_state_class_sensor.test": {
|
||||
"issue_type": "unsupported_state_class",
|
||||
"state_class": None,
|
||||
"statistic_id": "sensor.test",
|
||||
}
|
||||
}
|
||||
assert_issues(hass, expected)
|
||||
|
||||
|
||||
async def async_record_meter_states(
|
||||
|
@ -425,10 +425,10 @@ async def test_caching(hass: HomeAssistant) -> None:
|
||||
side_effect=translation.build_resources,
|
||||
) as mock_build_resources:
|
||||
load1 = await translation.async_get_translations(hass, "en", "entity_component")
|
||||
assert len(mock_build_resources.mock_calls) == 6
|
||||
assert len(mock_build_resources.mock_calls) == 7
|
||||
|
||||
load2 = await translation.async_get_translations(hass, "en", "entity_component")
|
||||
assert len(mock_build_resources.mock_calls) == 6
|
||||
assert len(mock_build_resources.mock_calls) == 7
|
||||
|
||||
assert load1 == load2
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user