1
mirror of https://github.com/home-assistant/core synced 2024-09-09 12:51:22 +02:00

Validate units when importing statistics (#78891)

This commit is contained in:
Erik Montnemery 2022-09-21 18:08:53 +02:00 committed by GitHub
parent e7b594b5cf
commit cd6697615f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 3 deletions

View File

@ -195,6 +195,17 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = {
VOLUME_CUBIC_METERS: "volume",
}
STATISTIC_UNIT_TO_VALID_UNITS: dict[str | None, Iterable[str | None]] = {
ENERGY_KILO_WATT_HOUR: [
ENERGY_KILO_WATT_HOUR,
ENERGY_MEGA_WATT_HOUR,
ENERGY_WATT_HOUR,
],
POWER_WATT: power_util.VALID_UNITS,
PRESSURE_PA: pressure_util.VALID_UNITS,
TEMP_CELSIUS: temperature_util.VALID_UNITS,
VOLUME_CUBIC_METERS: volume_util.VALID_UNITS,
}
# Convert energy power, pressure, temperature and volume statistics from the
# normalized unit used for statistics to the unit configured by the user
@ -238,8 +249,17 @@ def _get_statistic_to_display_unit_converter(
) is None:
return no_conversion
display_unit: str | None
unit_class = STATISTIC_UNIT_TO_UNIT_CLASS[statistic_unit]
display_unit = requested_units.get(unit_class) if requested_units else state_unit
if requested_units and unit_class in requested_units:
display_unit = requested_units[unit_class]
else:
display_unit = state_unit
if display_unit not in STATISTIC_UNIT_TO_VALID_UNITS[statistic_unit]:
# Guard against invalid state unit in the DB
return no_conversion
return partial(convert_fn, display_unit)
@ -1503,6 +1523,16 @@ def _async_import_statistics(
get_instance(hass).async_import_statistics(metadata, statistics)
def _validate_units(statistics_unit: str | None, state_unit: str | None) -> None:
"""Raise if the statistics unit and state unit are not compatible."""
if statistics_unit == state_unit:
return
if (valid_units := STATISTIC_UNIT_TO_VALID_UNITS.get(statistics_unit)) is None:
raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}")
if state_unit not in valid_units:
raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}")
@callback
def async_import_statistics(
hass: HomeAssistant,
@ -1520,6 +1550,10 @@ def async_import_statistics(
if not metadata["source"] or metadata["source"] != DOMAIN:
raise HomeAssistantError("Invalid source")
_validate_units(
metadata["unit_of_measurement"], metadata["state_unit_of_measurement"]
)
_async_import_statistics(hass, metadata, statistics)
@ -1542,6 +1576,10 @@ def async_add_external_statistics(
if not metadata["source"] or metadata["source"] != domain:
raise HomeAssistantError("Invalid source")
_validate_units(
metadata["unit_of_measurement"], metadata["state_unit_of_measurement"]
)
_async_import_statistics(hass, metadata, statistics)

View File

@ -96,6 +96,7 @@ async def test_demo_statistics_growth(hass, recorder_mock):
metadata = {
"source": DOMAIN,
"name": "Energy consumption 1",
"state_unit_of_measurement": "",
"statistic_id": statistic_id,
"unit_of_measurement": "",
"has_mean": False,

View File

@ -343,6 +343,7 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_1",
"unit_of_measurement": "kWh",
}
@ -377,6 +378,7 @@ async def test_fossil_energy_consumption_no_co2(hass, hass_ws_client, recorder_m
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_2",
"unit_of_measurement": "kWh",
}
@ -504,6 +506,7 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_1",
"unit_of_measurement": "kWh",
}
@ -538,6 +541,7 @@ async def test_fossil_energy_consumption_hole(hass, hass_ws_client, recorder_moc
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_2",
"unit_of_measurement": "kWh",
}
@ -663,6 +667,7 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_1",
"unit_of_measurement": "kWh",
}
@ -697,6 +702,7 @@ async def test_fossil_energy_consumption_no_data(hass, hass_ws_client, recorder_
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_2",
"unit_of_measurement": "kWh",
}
@ -813,6 +819,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock):
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_1",
"unit_of_measurement": "kWh",
}
@ -847,6 +854,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock):
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import_tariff_2",
"unit_of_measurement": "kWh",
}
@ -877,6 +885,7 @@ async def test_fossil_energy_consumption(hass, hass_ws_client, recorder_mock):
"has_sum": False,
"name": "Fossil percentage",
"source": "test",
"state_unit_of_measurement": "%",
"statistic_id": "test:fossil_percentage",
"unit_of_measurement": "%",
}

View File

@ -741,6 +741,7 @@ def test_external_statistics_errors(hass_recorder, caplog):
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import",
"unit_of_measurement": "kWh",
}
@ -804,6 +805,16 @@ def test_external_statistics_errors(hass_recorder, caplog):
assert list_statistic_ids(hass) == []
assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {}
# Attempt to insert statistics with an invalid unit combination
external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"}
external_statistics = {**_external_statistics}
with pytest.raises(HomeAssistantError):
async_add_external_statistics(hass, external_metadata, (external_statistics,))
wait_recording_done(hass)
assert statistics_during_period(hass, zero, period="hour") == {}
assert list_statistic_ids(hass) == []
assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {}
def test_import_statistics_errors(hass_recorder, caplog):
"""Test validation of imported statistics."""
@ -828,6 +839,7 @@ def test_import_statistics_errors(hass_recorder, caplog):
"has_sum": True,
"name": "Total imported energy",
"source": "recorder",
"state_unit_of_measurement": "kWh",
"statistic_id": "sensor.total_energy_import",
"unit_of_measurement": "kWh",
}
@ -891,6 +903,16 @@ def test_import_statistics_errors(hass_recorder, caplog):
assert list_statistic_ids(hass) == []
assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {}
# Attempt to insert statistics with an invalid unit combination
external_metadata = {**_external_metadata, "state_unit_of_measurement": "cats"}
external_statistics = {**_external_statistics}
with pytest.raises(HomeAssistantError):
async_import_statistics(hass, external_metadata, (external_statistics,))
wait_recording_done(hass)
assert statistics_during_period(hass, zero, period="hour") == {}
assert list_statistic_ids(hass) == []
assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {}
@pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"])
@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
@ -940,6 +962,7 @@ def test_monthly_statistics(hass_recorder, caplog, timezone):
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": "kWh",
"statistic_id": "test:total_energy_import",
"unit_of_measurement": "kWh",
}

View File

@ -1100,6 +1100,7 @@ async def test_get_statistics_metadata(
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"state_unit_of_measurement": unit,
"statistic_id": "test:total_gas",
"unit_of_measurement": unit,
}
@ -1107,6 +1108,29 @@ async def test_get_statistics_metadata(
async_add_external_statistics(
hass, external_energy_metadata_1, external_energy_statistics_1
)
await async_wait_recording_done(hass)
await client.send_json(
{
"id": 2,
"type": "recorder/get_statistics_metadata",
"statistic_ids": ["test:total_gas"],
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
{
"statistic_id": "test:total_gas",
"display_unit_of_measurement": unit,
"has_mean": False,
"has_sum": True,
"name": "Total imported energy",
"source": "test",
"statistics_unit_of_measurement": unit,
"unit_class": unit_class,
}
]
hass.states.async_set("sensor.test", 10, attributes=attributes)
await async_wait_recording_done(hass)
@ -1116,7 +1140,7 @@ async def test_get_statistics_metadata(
await client.send_json(
{
"id": 2,
"id": 3,
"type": "recorder/get_statistics_metadata",
"statistic_ids": ["sensor.test"],
}
@ -1144,7 +1168,7 @@ async def test_get_statistics_metadata(
await client.send_json(
{
"id": 3,
"id": 4,
"type": "recorder/get_statistics_metadata",
"statistic_ids": ["sensor.test"],
}