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
9 changed files with 395 additions and 109 deletions
|
@ -62,13 +62,15 @@ LAST_REPORTED_SCHEMA_VERSION = 43
|
||||||
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28
|
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28
|
||||||
|
|
||||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics"
|
INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics"
|
||||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS = "validate_statistics"
|
|
||||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS = "list_statistic_ids"
|
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_METHODS = {
|
||||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS,
|
INTEGRATION_PLATFORM_COMPILE_STATISTICS,
|
||||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS,
|
|
||||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS,
|
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,
|
EVENT_RECORDER_HOURLY_STATISTICS_GENERATED,
|
||||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS,
|
INTEGRATION_PLATFORM_COMPILE_STATISTICS,
|
||||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS,
|
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS,
|
||||||
|
INTEGRATION_PLATFORM_UPDATE_STATISTICS_ISSUES,
|
||||||
INTEGRATION_PLATFORM_VALIDATE_STATISTICS,
|
INTEGRATION_PLATFORM_VALIDATE_STATISTICS,
|
||||||
SupportedDialect,
|
SupportedDialect,
|
||||||
)
|
)
|
||||||
|
@ -586,6 +587,17 @@ def _compile_statistics(
|
||||||
):
|
):
|
||||||
new_short_term_stats.append(new_stat)
|
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:
|
if start.minute == 55:
|
||||||
# A full hour is ready, summarize it
|
# A full hour is ready, summarize it
|
||||||
_compile_hourly_statistics(session, start)
|
_compile_hourly_statistics(session, start)
|
||||||
|
@ -2212,6 +2224,16 @@ def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]
|
||||||
return platform_validation
|
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(
|
def _statistics_exists(
|
||||||
session: Session,
|
session: Session,
|
||||||
table: type[StatisticsBase],
|
table: type[StatisticsBase],
|
||||||
|
|
|
@ -43,6 +43,7 @@ from .statistics import (
|
||||||
list_statistic_ids,
|
list_statistic_ids,
|
||||||
statistic_during_period,
|
statistic_during_period,
|
||||||
statistics_during_period,
|
statistics_during_period,
|
||||||
|
update_statistics_issues,
|
||||||
validate_statistics,
|
validate_statistics,
|
||||||
)
|
)
|
||||||
from .util import PERIOD_SCHEMA, get_instance, resolve_period
|
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_get_statistics_metadata)
|
||||||
websocket_api.async_register_command(hass, ws_list_statistic_ids)
|
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_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_update_statistics_metadata)
|
||||||
websocket_api.async_register_command(hass, ws_validate_statistics)
|
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)
|
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.require_admin
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
import datetime
|
import datetime
|
||||||
|
from functools import partial
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
@ -30,8 +31,9 @@ from homeassistant.const import (
|
||||||
UnitOfSoundPressure,
|
UnitOfSoundPressure,
|
||||||
UnitOfVolume,
|
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.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.entity import entity_sources
|
from homeassistant.helpers.entity import entity_sources
|
||||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
from homeassistant.loader import async_suggest_report_issue
|
from homeassistant.loader import async_suggest_report_issue
|
||||||
|
@ -672,6 +674,113 @@ def list_statistic_ids(
|
||||||
return result
|
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(
|
def validate_statistics(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
) -> dict[str, list[statistics.ValidationIssue]]:
|
) -> dict[str, list[statistics.ValidationIssue]]:
|
||||||
|
@ -685,14 +794,28 @@ def validate_statistics(
|
||||||
instance = get_instance(hass)
|
instance = get_instance(hass)
|
||||||
entity_filter = instance.entity_filter
|
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:
|
for state in sensor_states:
|
||||||
entity_id = state.entity_id
|
entity_id = state.entity_id
|
||||||
state_class = try_parse_enum(
|
state_class = try_parse_enum(
|
||||||
SensorStateClass, state.attributes.get(ATTR_STATE_CLASS)
|
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):
|
if entity_filter and not entity_filter(state.entity_id):
|
||||||
# Sensor was previously recorded, but no longer is
|
# Sensor was previously recorded, but no longer is
|
||||||
validation_result[entity_id].append(
|
validation_result[entity_id].append(
|
||||||
|
@ -701,47 +824,6 @@ def validate_statistics(
|
||||||
{"statistic_id": entity_id},
|
{"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:
|
elif state_class is not None:
|
||||||
if entity_filter and not entity_filter(state.entity_id):
|
if entity_filter and not entity_filter(state.entity_id):
|
||||||
# Sensor is not recorded
|
# Sensor is not recorded
|
||||||
|
|
|
@ -287,5 +287,15 @@
|
||||||
"wind_speed": {
|
"wind_speed": {
|
||||||
"name": "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(
|
recorder_platform = Mock(
|
||||||
compile_statistics=Mock(wraps=_mock_compile_statistics),
|
compile_statistics=Mock(wraps=_mock_compile_statistics),
|
||||||
list_statistic_ids=Mock(wraps=_mock_list_statistic_ids),
|
list_statistic_ids=Mock(wraps=_mock_list_statistic_ids),
|
||||||
|
update_statistics_issues=Mock(),
|
||||||
validate_statistics=Mock(wraps=_mock_validate_statistics),
|
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.compile_statistics.assert_not_called()
|
||||||
recorder_platform.list_statistic_ids.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()
|
recorder_platform.validate_statistics.assert_not_called()
|
||||||
|
|
||||||
# Test compile statistics
|
# Test compile statistics + update statistics issues
|
||||||
zero = get_start_time(dt_util.utcnow())
|
# 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)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
recorder_platform.compile_statistics.assert_called_once_with(
|
recorder_platform.compile_statistics.assert_called_once_with(
|
||||||
hass, ANY, zero, zero + timedelta(minutes=5)
|
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.list_statistic_ids.assert_not_called()
|
||||||
recorder_platform.validate_statistics.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(
|
recorder_platform.list_statistic_ids.assert_called_once_with(
|
||||||
hass, statistic_ids=None, statistic_type=None
|
hass, statistic_ids=None, statistic_type=None
|
||||||
)
|
)
|
||||||
|
recorder_platform.update_statistics_issues.assert_called_once()
|
||||||
recorder_platform.validate_statistics.assert_not_called()
|
recorder_platform.validate_statistics.assert_not_called()
|
||||||
|
|
||||||
# Test validate statistics
|
# Test validate statistics
|
||||||
|
@ -2551,6 +2557,7 @@ async def test_recorder_platform_with_statistics(
|
||||||
)
|
)
|
||||||
recorder_platform.compile_statistics.assert_called_once()
|
recorder_platform.compile_statistics.assert_called_once()
|
||||||
recorder_platform.list_statistic_ids.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)
|
recorder_platform.validate_statistics.assert_called_once_with(hass)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2575,6 +2582,7 @@ async def test_recorder_platform_without_statistics(
|
||||||
[
|
[
|
||||||
("compile_statistics",),
|
("compile_statistics",),
|
||||||
("list_statistic_ids",),
|
("list_statistic_ids",),
|
||||||
|
("update_statistics_issues",),
|
||||||
("validate_statistics",),
|
("validate_statistics",),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -2601,6 +2609,7 @@ async def test_recorder_platform_with_partial_statistics_support(
|
||||||
mock_impl = {
|
mock_impl = {
|
||||||
"compile_statistics": _mock_compile_statistics,
|
"compile_statistics": _mock_compile_statistics,
|
||||||
"list_statistic_ids": _mock_list_statistic_ids,
|
"list_statistic_ids": _mock_list_statistic_ids,
|
||||||
|
"update_statistics_issues": None,
|
||||||
"validate_statistics": _mock_validate_statistics,
|
"validate_statistics": _mock_validate_statistics,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2620,8 +2629,10 @@ async def test_recorder_platform_with_partial_statistics_support(
|
||||||
for meth in supported_methods:
|
for meth in supported_methods:
|
||||||
getattr(recorder_platform, meth).assert_not_called()
|
getattr(recorder_platform, meth).assert_not_called()
|
||||||
|
|
||||||
# Test compile statistics
|
# Test compile statistics + update statistics issues
|
||||||
zero = get_start_time(dt_util.utcnow())
|
# 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)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
|
|
|
@ -1984,6 +1984,18 @@ async def test_validate_statistics(
|
||||||
await assert_validation_result(client, {})
|
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(
|
async def test_clear_statistics(
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
"""The tests for sensor recorder platform."""
|
"""The tests for sensor recorder platform."""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import math
|
import math
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from unittest.mock import patch
|
from unittest.mock import ANY, patch
|
||||||
|
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
from freezegun.api import FrozenDateTimeFactory
|
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.components.sensor import ATTR_OPTIONS, DOMAIN, SensorDeviceClass
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE
|
from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
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."""
|
"""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(
|
async def async_list_statistic_ids(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
statistic_ids: set[str] | None = None,
|
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(
|
async def assert_validation_result(
|
||||||
|
hass: HomeAssistant,
|
||||||
client: MockHAClientWebSocket,
|
client: MockHAClientWebSocket,
|
||||||
expected_result: dict[str, list[dict[str, Any]]],
|
expected_validation_result: dict[str, list[dict[str, Any]]],
|
||||||
|
expected_issues: Iterable[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Assert statistics validation result."""
|
"""Assert statistics validation result."""
|
||||||
await client.send_json_auto_id({"type": "recorder/validate_statistics"})
|
await client.send_json_auto_id({"type": "recorder/validate_statistics"})
|
||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4219,7 +4285,7 @@ async def test_validate_unit_change_convertible(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# 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
|
# No statistics, unit in state matching device class - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4229,7 +4295,7 @@ async def test_validate_unit_change_convertible(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# No statistics, unit in state not matching device class - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4239,7 +4305,7 @@ async def test_validate_unit_change_convertible(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Statistics has run, incompatible unit - expect error
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4274,12 +4340,12 @@ async def test_validate_unit_change_convertible(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state, statistic runs again - empty response
|
||||||
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state in compatible unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4289,12 +4355,12 @@ async def test_validate_unit_change_convertible(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state, statistic runs again - empty response
|
||||||
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Remove the state - expect error about missing state
|
||||||
hass.states.async_remove("sensor.test")
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4333,7 +4399,7 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, no device class - empty response
|
# No statistics, no device class - empty response
|
||||||
initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"}
|
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()
|
"sensor.test", 10, attributes=initial_attributes, timestamp=now.timestamp()
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
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
|
# Statistics has run, device class set not matching unit - empty response
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
|
@ -4353,7 +4419,7 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4418,7 +4484,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, sensor state set - empty response
|
# No statistics, sensor state set - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4428,7 +4494,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# No statistics, sensor state set to an incompatible unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4438,7 +4504,7 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Statistics has run, incompatible unit - expect error
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4473,12 +4539,12 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state, statistic runs again - empty response
|
||||||
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
do_adhoc_statistics(hass, start=now + timedelta(hours=1))
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state in compatible unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4488,12 +4554,12 @@ async def test_validate_statistics_unit_change_no_device_class(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state, statistic runs again - empty response
|
||||||
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Remove the state - expect error about missing state
|
||||||
hass.states.async_remove("sensor.test")
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4530,19 +4596,19 @@ async def test_validate_statistics_unsupported_state_class(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, valid state - empty response
|
# No statistics, valid state - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Statistics has run, empty response
|
# Statistics has run, empty response
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# State update with invalid state class, expect error
|
||||||
_attributes = dict(attributes)
|
_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(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4587,19 +4653,19 @@ async def test_validate_statistics_sensor_no_longer_recorded(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, valid state - empty response
|
# No statistics, valid state - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Statistics has run, empty response
|
# Statistics has run, empty response
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Sensor no longer recorded, expect error
|
# Sensor no longer recorded, expect error
|
||||||
expected = {
|
expected = {
|
||||||
|
@ -4616,7 +4682,7 @@ async def test_validate_statistics_sensor_no_longer_recorded(
|
||||||
"entity_filter",
|
"entity_filter",
|
||||||
return_value=False,
|
return_value=False,
|
||||||
):
|
):
|
||||||
await assert_validation_result(client, expected)
|
await assert_validation_result(hass, client, expected, {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4641,7 +4707,7 @@ async def test_validate_statistics_sensor_not_recorded(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Sensor not recorded, expect error
|
# Sensor not recorded, expect error
|
||||||
expected = {
|
expected = {
|
||||||
|
@ -4662,12 +4728,12 @@ async def test_validate_statistics_sensor_not_recorded(
|
||||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
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
|
# Statistics has run, expect same error
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, expected)
|
await assert_validation_result(hass, client, expected, {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4692,19 +4758,19 @@ async def test_validate_statistics_sensor_removed(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, valid state - empty response
|
# No statistics, valid state - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
"sensor.test", 10, attributes=attributes, timestamp=now.timestamp()
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Statistics has run, empty response
|
# Statistics has run, empty response
|
||||||
do_adhoc_statistics(hass, start=now)
|
do_adhoc_statistics(hass, start=now)
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Sensor removed, expect error
|
# Sensor removed, expect error
|
||||||
hass.states.async_remove("sensor.test")
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4741,7 +4807,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, original unit - empty response
|
# No statistics, original unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4750,7 +4816,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||||
attributes={**attributes, "unit_of_measurement": unit1},
|
attributes={**attributes, "unit_of_measurement": unit1},
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, changed unit - empty response
|
# No statistics, changed unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4759,7 +4825,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||||
attributes={**attributes, "unit_of_measurement": unit2},
|
attributes={**attributes, "unit_of_measurement": unit2},
|
||||||
timestamp=now.timestamp(),
|
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
|
# Run statistics, no statistics will be generated because of conflicting units
|
||||||
await async_recorder_block_till_done(hass)
|
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},
|
attributes={**attributes, "unit_of_measurement": unit1},
|
||||||
timestamp=now.timestamp(),
|
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
|
# Run statistics one hour later, only the state with unit1 will be considered
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
|
@ -4783,7 +4849,7 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||||
await assert_statistic_ids(
|
await assert_statistic_ids(
|
||||||
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
|
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
|
||||||
)
|
)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Change unit - expect error
|
# Change unit - expect error
|
||||||
hass.states.async_set(
|
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
|
# Original unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4816,13 +4882,13 @@ async def test_validate_statistics_unit_change_no_conversion(
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Valid state, statistic runs again - empty response
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
do_adhoc_statistics(hass, start=now + timedelta(hours=2))
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Remove the state - expect error
|
# Remove the state - expect error
|
||||||
hass.states.async_remove("sensor.test")
|
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(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4864,7 +4930,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, original unit - empty response
|
# No statistics, original unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4873,7 +4939,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||||
attributes={**attributes, "unit_of_measurement": unit1},
|
attributes={**attributes, "unit_of_measurement": unit1},
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Run statistics
|
# Run statistics
|
||||||
await async_recorder_block_till_done(hass)
|
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},
|
attributes={**attributes, "unit_of_measurement": unit2},
|
||||||
timestamp=now.timestamp() + 1,
|
timestamp=now.timestamp() + 1,
|
||||||
)
|
)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Run statistics one hour later, metadata will be updated
|
# Run statistics one hour later, metadata will be updated
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
|
@ -4899,7 +4965,7 @@ async def test_validate_statistics_unit_change_equivalent_units(
|
||||||
await assert_statistic_ids(
|
await assert_statistic_ids(
|
||||||
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit2}]
|
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit2}]
|
||||||
)
|
)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -4928,7 +4994,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2(
|
||||||
client = await hass_ws_client()
|
client = await hass_ws_client()
|
||||||
|
|
||||||
# No statistics, no state - empty response
|
# No statistics, no state - empty response
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# No statistics, original unit - empty response
|
# No statistics, original unit - empty response
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -4937,7 +5003,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2(
|
||||||
attributes={**attributes, "unit_of_measurement": unit1},
|
attributes={**attributes, "unit_of_measurement": unit1},
|
||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
await assert_validation_result(client, {})
|
await assert_validation_result(hass, client, {}, {})
|
||||||
|
|
||||||
# Run statistics
|
# Run statistics
|
||||||
await async_recorder_block_till_done(hass)
|
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
|
# Run statistics one hour later, metadata will not be updated
|
||||||
await async_recorder_block_till_done(hass)
|
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(
|
await assert_statistic_ids(
|
||||||
hass, [{"statistic_id": "sensor.test", "unit_of_measurement": unit1}]
|
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(
|
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)
|
await async_recorder_block_till_done(hass)
|
||||||
|
|
||||||
# We should not get complains about the missing number entity
|
# 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(
|
async def async_record_meter_states(
|
||||||
|
|
|
@ -425,10 +425,10 @@ async def test_caching(hass: HomeAssistant) -> None:
|
||||||
side_effect=translation.build_resources,
|
side_effect=translation.build_resources,
|
||||||
) as mock_build_resources:
|
) as mock_build_resources:
|
||||||
load1 = await translation.async_get_translations(hass, "en", "entity_component")
|
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")
|
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
|
assert load1 == load2
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue