mirror of
https://github.com/home-assistant/core.git
synced 2025-11-13 13:00:11 +00:00
Improve unit handling in recorder (#153941)
This commit is contained in:
@@ -29,6 +29,7 @@ from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
async_import_statistics,
|
||||
async_list_statistic_ids,
|
||||
async_update_statistics_metadata,
|
||||
get_last_short_term_statistics,
|
||||
get_last_statistics,
|
||||
get_latest_short_term_statistics_with_session,
|
||||
@@ -48,6 +49,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .common import (
|
||||
assert_dict_of_states_equal_without_context_and_last_changed,
|
||||
@@ -63,6 +65,12 @@ from tests.common import MockPlatform, MockUser, mock_platform
|
||||
from tests.typing import RecorderInstanceContextManager, WebSocketGenerator
|
||||
from tests.util.test_unit_conversion import _ALL_CONVERTERS
|
||||
|
||||
POWER_SENSOR_KW_ATTRIBUTES = {
|
||||
"device_class": "power",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "kW",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multiple_start_time_chunk_sizes(
|
||||
@@ -397,6 +405,7 @@ def mock_sensor_statistics():
|
||||
"has_sum": False,
|
||||
"name": None,
|
||||
"statistic_id": entity_id,
|
||||
"unit_class": None,
|
||||
"unit_of_measurement": "dogs",
|
||||
},
|
||||
"stat": {"start": start},
|
||||
@@ -839,7 +848,18 @@ async def test_statistics_duplicated(
|
||||
caplog.clear()
|
||||
|
||||
|
||||
# Integration frame mocked because of deprecation warnings about missing
|
||||
# unit_class, can be removed in HA Core 2025.11
|
||||
@pytest.mark.parametrize("integration_frame_path", ["custom_components/my_integration"])
|
||||
@pytest.mark.usefixtures("mock_integration_frame")
|
||||
@pytest.mark.parametrize("last_reset_str", ["2022-01-01T00:00:00+02:00", None])
|
||||
@pytest.mark.parametrize(
|
||||
("external_metadata_extra"),
|
||||
[
|
||||
{},
|
||||
{"unit_class": "energy"},
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("source", "statistic_id", "import_fn"),
|
||||
[
|
||||
@@ -852,6 +872,7 @@ async def test_import_statistics(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
external_metadata_extra: dict[str, str],
|
||||
source,
|
||||
statistic_id,
|
||||
import_fn,
|
||||
@@ -889,7 +910,7 @@ async def test_import_statistics(
|
||||
"source": source,
|
||||
"statistic_id": statistic_id,
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
} | external_metadata_extra
|
||||
|
||||
import_fn(hass, external_metadata, (external_statistics1, external_statistics2))
|
||||
await async_wait_recording_done(hass)
|
||||
@@ -939,6 +960,7 @@ async def test_import_statistics(
|
||||
"name": "Total imported energy",
|
||||
"source": source,
|
||||
"statistic_id": statistic_id,
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
},
|
||||
)
|
||||
@@ -1031,6 +1053,7 @@ async def test_import_statistics(
|
||||
"name": "Total imported energy renamed",
|
||||
"source": source,
|
||||
"statistic_id": statistic_id,
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
},
|
||||
)
|
||||
@@ -1119,6 +1142,7 @@ async def test_external_statistics_errors(
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -1207,6 +1231,7 @@ async def test_import_statistics_errors(
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -1270,6 +1295,213 @@ async def test_import_statistics_errors(
|
||||
assert get_metadata(hass, statistic_ids={"sensor.total_energy_import"}) == {}
|
||||
|
||||
|
||||
# Integration frame mocked because of deprecation warnings about missing
|
||||
# unit_class, can be removed in HA Core 2025.11
|
||||
@pytest.mark.parametrize("integration_frame_path", ["custom_components/my_integration"])
|
||||
@pytest.mark.usefixtures("mock_integration_frame")
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"requested_new_unit",
|
||||
"update_statistics_extra",
|
||||
"new_unit",
|
||||
"new_unit_class",
|
||||
"new_display_unit",
|
||||
),
|
||||
[
|
||||
("dogs", {}, "dogs", None, "dogs"),
|
||||
("dogs", {"new_unit_class": None}, "dogs", None, "dogs"),
|
||||
(None, {}, None, "unitless", None),
|
||||
(None, {"new_unit_class": "unitless"}, None, "unitless", None),
|
||||
("W", {}, "W", "power", "kW"),
|
||||
("W", {"new_unit_class": "power"}, "W", "power", "kW"),
|
||||
# Note: Display unit is guessed even if unit_class is None
|
||||
("W", {"new_unit_class": None}, "W", None, "kW"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_update_statistics_metadata(
|
||||
hass: HomeAssistant,
|
||||
requested_new_unit,
|
||||
update_statistics_extra,
|
||||
new_unit,
|
||||
new_unit_class,
|
||||
new_display_unit,
|
||||
) -> None:
|
||||
"""Test removing statistics."""
|
||||
now = get_start_time(dt_util.utcnow())
|
||||
|
||||
units = METRIC_SYSTEM
|
||||
attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None}
|
||||
state = 10
|
||||
|
||||
hass.config.units = units
|
||||
await async_setup_component(hass, "sensor", {})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set(
|
||||
"sensor.test", state, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
do_adhoc_statistics(hass, period="hourly", start=now)
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
statistic_ids = await async_list_statistic_ids(hass)
|
||||
assert statistic_ids == [
|
||||
{
|
||||
"statistic_id": "sensor.test",
|
||||
"display_unit_of_measurement": "kW",
|
||||
"has_mean": True,
|
||||
"mean_type": StatisticMeanType.ARITHMETIC,
|
||||
"has_sum": False,
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": "kW",
|
||||
"unit_class": "power",
|
||||
}
|
||||
]
|
||||
|
||||
async_update_statistics_metadata(
|
||||
hass,
|
||||
"sensor.test",
|
||||
new_unit_of_measurement=requested_new_unit,
|
||||
**update_statistics_extra,
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
statistic_ids = await async_list_statistic_ids(hass)
|
||||
assert statistic_ids == [
|
||||
{
|
||||
"statistic_id": "sensor.test",
|
||||
"display_unit_of_measurement": new_display_unit,
|
||||
"has_mean": True,
|
||||
"mean_type": StatisticMeanType.ARITHMETIC,
|
||||
"has_sum": False,
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": new_unit,
|
||||
"unit_class": new_unit_class,
|
||||
}
|
||||
]
|
||||
|
||||
assert statistics_during_period(
|
||||
hass,
|
||||
now,
|
||||
period="5minute",
|
||||
statistic_ids={"sensor.test"},
|
||||
units={"power": "W"},
|
||||
) == {
|
||||
"sensor.test": [
|
||||
{
|
||||
"end": (now + timedelta(minutes=5)).timestamp(),
|
||||
"last_reset": None,
|
||||
"max": 10.0,
|
||||
"mean": 10.0,
|
||||
"min": 10.0,
|
||||
"start": now.timestamp(),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"requested_new_unit",
|
||||
"update_statistics_extra",
|
||||
"error_message",
|
||||
),
|
||||
[
|
||||
("dogs", {"new_unit_class": "cats"}, "Unsupported unit_class: 'cats'"),
|
||||
(
|
||||
"dogs",
|
||||
{"new_unit_class": "power"},
|
||||
"Unsupported unit_of_measurement 'dogs' for unit_class 'power'",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_update_statistics_metadata_error(
|
||||
hass: HomeAssistant,
|
||||
requested_new_unit,
|
||||
update_statistics_extra,
|
||||
error_message,
|
||||
) -> None:
|
||||
"""Test removing statistics."""
|
||||
now = get_start_time(dt_util.utcnow())
|
||||
|
||||
units = METRIC_SYSTEM
|
||||
attributes = POWER_SENSOR_KW_ATTRIBUTES | {"device_class": None}
|
||||
state = 10
|
||||
|
||||
hass.config.units = units
|
||||
await async_setup_component(hass, "sensor", {})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set(
|
||||
"sensor.test", state, attributes=attributes, timestamp=now.timestamp()
|
||||
)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
do_adhoc_statistics(hass, period="hourly", start=now)
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
statistic_ids = await async_list_statistic_ids(hass)
|
||||
assert statistic_ids == [
|
||||
{
|
||||
"statistic_id": "sensor.test",
|
||||
"display_unit_of_measurement": "kW",
|
||||
"has_mean": True,
|
||||
"mean_type": StatisticMeanType.ARITHMETIC,
|
||||
"has_sum": False,
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": "kW",
|
||||
"unit_class": "power",
|
||||
}
|
||||
]
|
||||
|
||||
with pytest.raises(HomeAssistantError, match=error_message):
|
||||
async_update_statistics_metadata(
|
||||
hass,
|
||||
"sensor.test",
|
||||
new_unit_of_measurement=requested_new_unit,
|
||||
**update_statistics_extra,
|
||||
)
|
||||
await async_recorder_block_till_done(hass)
|
||||
|
||||
statistic_ids = await async_list_statistic_ids(hass)
|
||||
assert statistic_ids == [
|
||||
{
|
||||
"statistic_id": "sensor.test",
|
||||
"display_unit_of_measurement": "kW",
|
||||
"has_mean": True,
|
||||
"mean_type": StatisticMeanType.ARITHMETIC,
|
||||
"has_sum": False,
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": "kW",
|
||||
"unit_class": "power",
|
||||
}
|
||||
]
|
||||
|
||||
assert statistics_during_period(
|
||||
hass,
|
||||
now,
|
||||
period="5minute",
|
||||
statistic_ids={"sensor.test"},
|
||||
units={"power": "W"},
|
||||
) == {
|
||||
"sensor.test": [
|
||||
{
|
||||
"end": (now + timedelta(minutes=5)).timestamp(),
|
||||
"last_reset": None,
|
||||
"max": 10000.0,
|
||||
"mean": 10000.0,
|
||||
"min": 10000.0,
|
||||
"start": now.timestamp(),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("multiple_start_time_chunk_sizes")
|
||||
@pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"])
|
||||
@pytest.mark.freeze_time("2022-10-01 00:00:00+00:00")
|
||||
@@ -1337,6 +1569,7 @@ async def test_daily_statistics_sum(
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -1518,6 +1751,7 @@ async def test_multiple_daily_statistics_sum(
|
||||
"name": "Total imported energy 1",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import2",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
external_metadata2 = {
|
||||
@@ -1526,6 +1760,7 @@ async def test_multiple_daily_statistics_sum(
|
||||
"name": "Total imported energy 2",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import1",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -1716,6 +1951,7 @@ async def test_weekly_statistics_mean(
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -1862,6 +2098,7 @@ async def test_weekly_statistics_sum(
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -2043,6 +2280,7 @@ async def test_monthly_statistics_sum(
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -2372,6 +2610,7 @@ async def test_change(
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -2708,6 +2947,7 @@ async def test_change_multiple(
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.total_energy_import1",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
external_metadata2 = {
|
||||
@@ -2716,6 +2956,7 @@ async def test_change_multiple(
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.total_energy_import2",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
async_import_statistics(hass, external_metadata1, external_statistics)
|
||||
@@ -3097,6 +3338,7 @@ async def test_change_with_none(
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
@@ -3651,6 +3893,7 @@ async def test_get_statistics_service(
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.total_energy_import1",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
external_metadata2 = {
|
||||
@@ -3659,6 +3902,7 @@ async def test_get_statistics_service(
|
||||
"name": "Total imported energy",
|
||||
"source": "recorder",
|
||||
"statistic_id": "sensor.total_energy_import2",
|
||||
"unit_class": "energy",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
async_import_statistics(hass, external_metadata1, external_statistics)
|
||||
|
||||
Reference in New Issue
Block a user