Deprecate history integration's statistics API (#78056)

This commit is contained in:
Erik Montnemery 2022-09-08 22:03:43 +02:00 committed by GitHub
parent c528a2d2cd
commit 7937bfeedb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 535 additions and 440 deletions

View file

@ -6,22 +6,22 @@ from datetime import datetime as dt, timedelta
from http import HTTPStatus from http import HTTPStatus
import logging import logging
import time import time
from typing import Literal, cast from typing import cast
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant.components import frontend, websocket_api from homeassistant.components import frontend, websocket_api
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.recorder import get_instance, history from homeassistant.components.recorder import (
get_instance,
history,
websocket_api as recorder_ws,
)
from homeassistant.components.recorder.filters import ( from homeassistant.components.recorder.filters import (
Filters, Filters,
sqlalchemy_filter_from_include_exclude_conf, sqlalchemy_filter_from_include_exclude_conf,
) )
from homeassistant.components.recorder.statistics import (
list_statistic_ids,
statistics_during_period,
)
from homeassistant.components.recorder.util import session_scope from homeassistant.components.recorder.util import session_scope
from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api import messages
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -68,23 +68,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
def _ws_get_statistics_during_period(
hass: HomeAssistant,
msg_id: int,
start_time: dt,
end_time: dt | None = None,
statistic_ids: list[str] | None = None,
period: Literal["5minute", "day", "hour", "month"] = "hour",
) -> str:
"""Fetch statistics and convert them to json in the executor."""
return JSON_DUMP(
messages.result_message(
msg_id,
statistics_during_period(hass, start_time, end_time, statistic_ids, period),
)
)
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required("type"): "history/statistics_during_period", vol.Required("type"): "history/statistics_during_period",
@ -99,46 +82,11 @@ async def ws_get_statistics_during_period(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None: ) -> None:
"""Handle statistics websocket command.""" """Handle statistics websocket command."""
start_time_str = msg["start_time"] _LOGGER.warning(
end_time_str = msg.get("end_time") "WS API 'history/statistics_during_period' is deprecated and will be removed in "
"Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead"
if start_time := dt_util.parse_datetime(start_time_str):
start_time = dt_util.as_utc(start_time)
else:
connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time")
return
if end_time_str:
if end_time := dt_util.parse_datetime(end_time_str):
end_time = dt_util.as_utc(end_time)
else:
connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time")
return
else:
end_time = None
connection.send_message(
await get_instance(hass).async_add_executor_job(
_ws_get_statistics_during_period,
hass,
msg["id"],
start_time,
end_time,
msg.get("statistic_ids"),
msg.get("period"),
)
)
def _ws_get_list_statistic_ids(
hass: HomeAssistant,
msg_id: int,
statistic_type: Literal["mean"] | Literal["sum"] | None = None,
) -> str:
"""Fetch a list of available statistic_id and convert them to json in the executor."""
return JSON_DUMP(
messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type))
) )
await recorder_ws.ws_handle_get_statistics_during_period(hass, connection, msg)
@websocket_api.websocket_command( @websocket_api.websocket_command(
@ -152,14 +100,11 @@ async def ws_get_list_statistic_ids(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None: ) -> None:
"""Fetch a list of available statistic_id.""" """Fetch a list of available statistic_id."""
connection.send_message( _LOGGER.warning(
await get_instance(hass).async_add_executor_job( "WS API 'history/list_statistic_ids' is deprecated and will be removed in "
_ws_get_list_statistic_ids, "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead"
hass,
msg["id"],
msg.get("statistic_type"),
)
) )
await recorder_ws.ws_handle_list_statistic_ids(hass, connection, msg)
def _ws_get_significant_states( def _ws_get_significant_states(

View file

@ -1,13 +1,17 @@
"""The Recorder websocket API.""" """The Recorder websocket API."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime as dt
import logging import logging
from typing import Literal
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import messages
from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.core import HomeAssistant, callback, valid_entity_id
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import JSON_DUMP
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import MAX_QUEUE_BACKLOG from .const import MAX_QUEUE_BACKLOG
@ -15,6 +19,7 @@ from .statistics import (
async_add_external_statistics, async_add_external_statistics,
async_import_statistics, async_import_statistics,
list_statistic_ids, list_statistic_ids,
statistics_during_period,
validate_statistics, validate_statistics,
) )
from .util import async_migration_in_progress, async_migration_is_live, get_instance from .util import async_migration_in_progress, async_migration_is_live, get_instance
@ -25,15 +30,125 @@ _LOGGER: logging.Logger = logging.getLogger(__package__)
@callback @callback
def async_setup(hass: HomeAssistant) -> None: def async_setup(hass: HomeAssistant) -> None:
"""Set up the recorder websocket API.""" """Set up the recorder websocket API."""
websocket_api.async_register_command(hass, ws_validate_statistics)
websocket_api.async_register_command(hass, ws_clear_statistics)
websocket_api.async_register_command(hass, ws_get_statistics_metadata)
websocket_api.async_register_command(hass, ws_update_statistics_metadata)
websocket_api.async_register_command(hass, ws_info)
websocket_api.async_register_command(hass, ws_backup_start)
websocket_api.async_register_command(hass, ws_backup_end)
websocket_api.async_register_command(hass, ws_adjust_sum_statistics) websocket_api.async_register_command(hass, ws_adjust_sum_statistics)
websocket_api.async_register_command(hass, ws_backup_end)
websocket_api.async_register_command(hass, ws_backup_start)
websocket_api.async_register_command(hass, ws_clear_statistics)
websocket_api.async_register_command(hass, ws_get_statistics_during_period)
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_import_statistics)
websocket_api.async_register_command(hass, ws_info)
websocket_api.async_register_command(hass, ws_update_statistics_metadata)
websocket_api.async_register_command(hass, ws_validate_statistics)
def _ws_get_statistics_during_period(
hass: HomeAssistant,
msg_id: int,
start_time: dt,
end_time: dt | None = None,
statistic_ids: list[str] | None = None,
period: Literal["5minute", "day", "hour", "month"] = "hour",
) -> str:
"""Fetch statistics and convert them to json in the executor."""
return JSON_DUMP(
messages.result_message(
msg_id,
statistics_during_period(hass, start_time, end_time, statistic_ids, period),
)
)
async def ws_handle_get_statistics_during_period(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Handle statistics websocket command."""
start_time_str = msg["start_time"]
end_time_str = msg.get("end_time")
if start_time := dt_util.parse_datetime(start_time_str):
start_time = dt_util.as_utc(start_time)
else:
connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time")
return
if end_time_str:
if end_time := dt_util.parse_datetime(end_time_str):
end_time = dt_util.as_utc(end_time)
else:
connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time")
return
else:
end_time = None
connection.send_message(
await get_instance(hass).async_add_executor_job(
_ws_get_statistics_during_period,
hass,
msg["id"],
start_time,
end_time,
msg.get("statistic_ids"),
msg.get("period"),
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/statistics_during_period",
vol.Required("start_time"): str,
vol.Optional("end_time"): str,
vol.Optional("statistic_ids"): [str],
vol.Required("period"): vol.Any("5minute", "hour", "day", "month"),
}
)
@websocket_api.async_response
async def ws_get_statistics_during_period(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Handle statistics websocket command."""
await ws_handle_get_statistics_during_period(hass, connection, msg)
def _ws_get_list_statistic_ids(
hass: HomeAssistant,
msg_id: int,
statistic_type: Literal["mean"] | Literal["sum"] | None = None,
) -> str:
"""Fetch a list of available statistic_id and convert them to json in the executor."""
return JSON_DUMP(
messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type))
)
async def ws_handle_list_statistic_ids(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Fetch a list of available statistic_id."""
connection.send_message(
await get_instance(hass).async_add_executor_job(
_ws_get_list_statistic_ids,
hass,
msg["id"],
msg.get("statistic_type"),
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "recorder/list_statistic_ids",
vol.Optional("statistic_type"): vol.Any("sum", "mean"),
}
)
@websocket_api.async_response
async def ws_list_statistic_ids(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Fetch a list of available statistic_id."""
await ws_handle_list_statistic_ids(hass, connection, msg)
@websocket_api.websocket_command( @websocket_api.websocket_command(

View file

@ -5,24 +5,24 @@ from http import HTTPStatus
import json import json
from unittest.mock import patch, sentinel from unittest.mock import patch, sentinel
from freezegun import freeze_time
import pytest import pytest
from pytest import approx
from homeassistant.components import history from homeassistant.components import history
from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.history import get_significant_states
from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.models import process_timestamp
from homeassistant.components.recorder.websocket_api import (
ws_handle_get_statistics_during_period,
ws_handle_list_statistic_ids,
)
from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.json import JSONEncoder
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 IMPERIAL_SYSTEM, METRIC_SYSTEM
from tests.components.recorder.common import ( from tests.components.recorder.common import (
async_recorder_block_till_done, async_recorder_block_till_done,
async_wait_recording_done, async_wait_recording_done,
do_adhoc_statistics,
wait_recording_done, wait_recording_done,
) )
@ -844,51 +844,13 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(
assert response_json[1][0]["entity_id"] == "light.cow" assert response_json[1][0]["entity_id"] == "light.cow"
POWER_SENSOR_ATTRIBUTES = { async def test_statistics_during_period(hass, hass_ws_client, recorder_mock, caplog):
"device_class": "power", """Test history/statistics_during_period forwards to recorder."""
"state_class": "measurement",
"unit_of_measurement": "kW",
}
PRESSURE_SENSOR_ATTRIBUTES = {
"device_class": "pressure",
"state_class": "measurement",
"unit_of_measurement": "hPa",
}
TEMPERATURE_SENSOR_ATTRIBUTES = {
"device_class": "temperature",
"state_class": "measurement",
"unit_of_measurement": "°C",
}
@pytest.mark.parametrize(
"units, attributes, state, value",
[
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50),
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10),
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312),
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000),
],
)
async def test_statistics_during_period(
hass, hass_ws_client, recorder_mock, units, attributes, state, value
):
"""Test statistics_during_period."""
now = dt_util.utcnow() now = dt_util.utcnow()
hass.config.units = units
await async_setup_component(hass, "history", {}) await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.test", state, attributes=attributes)
await async_wait_recording_done(hass)
do_adhoc_statistics(hass, start=now)
await async_wait_recording_done(hass)
client = await hass_ws_client() client = await hass_ws_client()
# Test the WS API works and issues a warning
await client.send_json( await client.send_json(
{ {
"id": 1, "id": 1,
@ -903,322 +865,53 @@ async def test_statistics_during_period(
assert response["success"] assert response["success"]
assert response["result"] == {} assert response["result"] == {}
await client.send_json( assert (
{ "WS API 'history/statistics_during_period' is deprecated and will be removed in "
"id": 2, "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead"
"type": "history/statistics_during_period", ) in caplog.text
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"], # Test the WS API forwards to recorder
"period": "5minute", with patch(
} "homeassistant.components.history.recorder_ws.ws_handle_get_statistics_during_period",
) wraps=ws_handle_get_statistics_during_period,
response = await client.receive_json() ) as ws_mock:
assert response["success"] await client.send_json(
assert response["result"] == {
"sensor.test": [
{ {
"statistic_id": "sensor.test", "id": 2,
"start": now.isoformat(), "type": "history/statistics_during_period",
"end": (now + timedelta(minutes=5)).isoformat(), "start_time": now.isoformat(),
"mean": approx(value), "end_time": now.isoformat(),
"min": approx(value), "statistic_ids": ["sensor.test"],
"max": approx(value), "period": "hour",
"last_reset": None,
"state": None,
"sum": None,
} }
] )
} await client.receive_json()
ws_mock.assert_awaited_once()
@pytest.mark.parametrize( async def test_list_statistic_ids(hass, hass_ws_client, recorder_mock, caplog):
"units, attributes, state, value", """Test history/list_statistic_ids forwards to recorder."""
[
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50),
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10),
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312),
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000),
],
)
async def test_statistics_during_period_in_the_past(
hass, hass_ws_client, recorder_mock, units, attributes, state, value
):
"""Test statistics_during_period in the past."""
hass.config.set_time_zone("UTC")
now = dt_util.utcnow().replace()
hass.config.units = units
await async_setup_component(hass, "history", {}) await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
past = now - timedelta(days=3)
with freeze_time(past):
hass.states.async_set("sensor.test", state, attributes=attributes)
await async_wait_recording_done(hass)
sensor_state = hass.states.get("sensor.test")
assert sensor_state.last_updated == past
stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0)
stats_start = past.replace(minute=55)
do_adhoc_statistics(hass, start=stats_start)
await async_wait_recording_done(hass)
client = await hass_ws_client() client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "history/statistics_during_period",
"start_time": now.isoformat(),
"end_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "hour",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
await client.send_json( # Test the WS API works and issues a warning
{
"id": 2,
"type": "history/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
past = now - timedelta(days=3, hours=1)
await client.send_json(
{
"id": 3,
"type": "history/statistics_during_period",
"start_time": past.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"sensor.test": [
{
"statistic_id": "sensor.test",
"start": stats_start.isoformat(),
"end": (stats_start + timedelta(minutes=5)).isoformat(),
"mean": approx(value),
"min": approx(value),
"max": approx(value),
"last_reset": None,
"state": None,
"sum": None,
}
]
}
start_of_day = stats_top_of_hour.replace(hour=0, minute=0)
await client.send_json(
{
"id": 4,
"type": "history/statistics_during_period",
"start_time": stats_top_of_hour.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "day",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"sensor.test": [
{
"statistic_id": "sensor.test",
"start": start_of_day.isoformat(),
"end": (start_of_day + timedelta(days=1)).isoformat(),
"mean": approx(value),
"min": approx(value),
"max": approx(value),
"last_reset": None,
"state": None,
"sum": None,
}
]
}
await client.send_json(
{
"id": 5,
"type": "history/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
async def test_statistics_during_period_bad_start_time(
hass, hass_ws_client, recorder_mock
):
"""Test statistics_during_period."""
await async_setup_component(
hass,
"history",
{"history": {}},
)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "history/statistics_during_period",
"start_time": "cats",
"period": "5minute",
}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_start_time"
async def test_statistics_during_period_bad_end_time(
hass, hass_ws_client, recorder_mock
):
"""Test statistics_during_period."""
now = dt_util.utcnow()
await async_setup_component(
hass,
"history",
{"history": {}},
)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "history/statistics_during_period",
"start_time": now.isoformat(),
"end_time": "dogs",
"period": "5minute",
}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_end_time"
@pytest.mark.parametrize(
"units, attributes, display_unit, statistics_unit",
[
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"),
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"),
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "°C"),
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "°C"),
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi", "Pa"),
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa", "Pa"),
],
)
async def test_list_statistic_ids(
hass,
hass_ws_client,
recorder_mock,
units,
attributes,
display_unit,
statistics_unit,
):
"""Test list_statistic_ids."""
now = dt_util.utcnow()
hass.config.units = units
await async_setup_component(hass, "history", {"history": {}})
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) await client.send_json({"id": 1, "type": "history/list_statistic_ids"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert response["result"] == [] assert response["result"] == []
hass.states.async_set("sensor.test", 10, attributes=attributes) assert (
await async_wait_recording_done(hass) "WS API 'history/list_statistic_ids' is deprecated and will be removed in "
"Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead"
) in caplog.text
await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) with patch(
response = await client.receive_json() "homeassistant.components.history.recorder_ws.ws_handle_list_statistic_ids",
assert response["success"] wraps=ws_handle_list_statistic_ids,
assert response["result"] == [ ) as ws_mock:
{ await client.send_json({"id": 2, "type": "history/list_statistic_ids"})
"statistic_id": "sensor.test", await client.receive_json()
"has_mean": True, ws_mock.assert_called_once()
"has_sum": False,
"name": None,
"source": "recorder",
"display_unit_of_measurement": display_unit,
"statistics_unit_of_measurement": statistics_unit,
}
]
do_adhoc_statistics(hass, start=now)
await async_recorder_block_till_done(hass)
# Remove the state, statistics will now be fetched from the database
hass.states.async_remove("sensor.test")
await hass.async_block_till_done()
await client.send_json({"id": 3, "type": "history/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
{
"statistic_id": "sensor.test",
"has_mean": True,
"has_sum": False,
"name": None,
"source": "recorder",
"display_unit_of_measurement": display_unit,
"statistics_unit_of_measurement": statistics_unit,
}
]
await client.send_json(
{"id": 4, "type": "history/list_statistic_ids", "statistic_type": "dogs"}
)
response = await client.receive_json()
assert not response["success"]
await client.send_json(
{"id": 5, "type": "history/list_statistic_ids", "statistic_type": "mean"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
{
"statistic_id": "sensor.test",
"has_mean": True,
"has_sum": False,
"name": None,
"source": "recorder",
"display_unit_of_measurement": display_unit,
"statistics_unit_of_measurement": statistics_unit,
}
]
await client.send_json(
{"id": 6, "type": "history/list_statistic_ids", "statistic_type": "sum"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
async def test_history_during_period(hass, hass_ws_client, recorder_mock): async def test_history_during_period(hass, hass_ws_client, recorder_mock):
@ -1239,7 +932,6 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock):
hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) hass.states.async_set("sensor.test", "on", attributes={"any": "attr"})
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
do_adhoc_statistics(hass, start=now)
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
client = await hass_ws_client() client = await hass_ws_client()
@ -1358,8 +1050,6 @@ async def test_history_during_period_impossible_conditions(
hass, hass_ws_client, recorder_mock hass, hass_ws_client, recorder_mock
): ):
"""Test history_during_period returns when condition cannot be true.""" """Test history_during_period returns when condition cannot be true."""
now = dt_util.utcnow()
await async_setup_component(hass, "history", {}) await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {}) await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass) await async_recorder_block_till_done(hass)
@ -1374,7 +1064,6 @@ async def test_history_during_period_impossible_conditions(
hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) hass.states.async_set("sensor.test", "on", attributes={"any": "attr"})
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
do_adhoc_statistics(hass, start=now)
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
after = dt_util.utcnow() after = dt_util.utcnow()
@ -1440,7 +1129,6 @@ async def test_history_during_period_significant_domain(
hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) hass.states.async_set("climate.test", "on", attributes={"temperature": "5"})
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
do_adhoc_statistics(hass, start=now)
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
client = await hass_ws_client() client = await hass_ws_client()
@ -1664,7 +1352,6 @@ async def test_history_during_period_with_use_include_order(
hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) hass.states.async_set("switch.excluded", "off", attributes={"any": "again"})
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
do_adhoc_statistics(hass, start=now)
await async_wait_recording_done(hass) await async_wait_recording_done(hass)
client = await hass_ws_client() client = await hass_ws_client()

View file

@ -4,6 +4,7 @@ from datetime import timedelta
import threading import threading
from unittest.mock import patch from unittest.mock import patch
from freezegun import freeze_time
import pytest import pytest
from pytest import approx from pytest import approx
@ -18,7 +19,7 @@ from homeassistant.components.recorder.statistics import (
from homeassistant.helpers import recorder as recorder_helper from homeassistant.helpers import recorder as recorder_helper
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 from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
from .common import ( from .common import (
async_recorder_block_till_done, async_recorder_block_till_done,
@ -34,6 +35,11 @@ POWER_SENSOR_ATTRIBUTES = {
"state_class": "measurement", "state_class": "measurement",
"unit_of_measurement": "kW", "unit_of_measurement": "kW",
} }
PRESSURE_SENSOR_ATTRIBUTES = {
"device_class": "pressure",
"state_class": "measurement",
"unit_of_measurement": "hPa",
}
TEMPERATURE_SENSOR_ATTRIBUTES = { TEMPERATURE_SENSOR_ATTRIBUTES = {
"device_class": "temperature", "device_class": "temperature",
"state_class": "measurement", "state_class": "measurement",
@ -51,6 +57,351 @@ GAS_SENSOR_ATTRIBUTES = {
} }
@pytest.mark.parametrize(
"units, attributes, state, value",
[
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50),
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10),
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312),
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000),
],
)
async def test_statistics_during_period(
hass, hass_ws_client, recorder_mock, units, attributes, state, value
):
"""Test statistics_during_period."""
now = dt_util.utcnow()
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)
await async_wait_recording_done(hass)
do_adhoc_statistics(hass, start=now)
await async_wait_recording_done(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "hour",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
await client.send_json(
{
"id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"sensor.test": [
{
"statistic_id": "sensor.test",
"start": now.isoformat(),
"end": (now + timedelta(minutes=5)).isoformat(),
"mean": approx(value),
"min": approx(value),
"max": approx(value),
"last_reset": None,
"state": None,
"sum": None,
}
]
}
@pytest.mark.parametrize(
"units, attributes, state, value",
[
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000),
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50),
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10),
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312),
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000),
],
)
async def test_statistics_during_period_in_the_past(
hass, hass_ws_client, recorder_mock, units, attributes, state, value
):
"""Test statistics_during_period in the past."""
hass.config.set_time_zone("UTC")
now = dt_util.utcnow().replace()
hass.config.units = units
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
past = now - timedelta(days=3)
with freeze_time(past):
hass.states.async_set("sensor.test", state, attributes=attributes)
await async_wait_recording_done(hass)
sensor_state = hass.states.get("sensor.test")
assert sensor_state.last_updated == past
stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0)
stats_start = past.replace(minute=55)
do_adhoc_statistics(hass, start=stats_start)
await async_wait_recording_done(hass)
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "hour",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
await client.send_json(
{
"id": 2,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
past = now - timedelta(days=3, hours=1)
await client.send_json(
{
"id": 3,
"type": "recorder/statistics_during_period",
"start_time": past.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"sensor.test": [
{
"statistic_id": "sensor.test",
"start": stats_start.isoformat(),
"end": (stats_start + timedelta(minutes=5)).isoformat(),
"mean": approx(value),
"min": approx(value),
"max": approx(value),
"last_reset": None,
"state": None,
"sum": None,
}
]
}
start_of_day = stats_top_of_hour.replace(hour=0, minute=0)
await client.send_json(
{
"id": 4,
"type": "recorder/statistics_during_period",
"start_time": stats_top_of_hour.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "day",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"sensor.test": [
{
"statistic_id": "sensor.test",
"start": start_of_day.isoformat(),
"end": (start_of_day + timedelta(days=1)).isoformat(),
"mean": approx(value),
"min": approx(value),
"max": approx(value),
"last_reset": None,
"state": None,
"sum": None,
}
]
}
await client.send_json(
{
"id": 5,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"statistic_ids": ["sensor.test"],
"period": "5minute",
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {}
async def test_statistics_during_period_bad_start_time(
hass, hass_ws_client, recorder_mock
):
"""Test statistics_during_period."""
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "recorder/statistics_during_period",
"start_time": "cats",
"period": "5minute",
}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_start_time"
async def test_statistics_during_period_bad_end_time(
hass, hass_ws_client, recorder_mock
):
"""Test statistics_during_period."""
now = dt_util.utcnow()
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "recorder/statistics_during_period",
"start_time": now.isoformat(),
"end_time": "dogs",
"period": "5minute",
}
)
response = await client.receive_json()
assert not response["success"]
assert response["error"]["code"] == "invalid_end_time"
@pytest.mark.parametrize(
"units, attributes, display_unit, statistics_unit",
[
(IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"),
(METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W", "W"),
(IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F", "°C"),
(METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C", "°C"),
(IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi", "Pa"),
(METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa", "Pa"),
],
)
async def test_list_statistic_ids(
hass,
hass_ws_client,
recorder_mock,
units,
attributes,
display_unit,
statistics_unit,
):
"""Test list_statistic_ids."""
now = dt_util.utcnow()
hass.config.units = units
await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass)
client = await hass_ws_client()
await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
hass.states.async_set("sensor.test", 10, attributes=attributes)
await async_wait_recording_done(hass)
await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
{
"statistic_id": "sensor.test",
"has_mean": True,
"has_sum": False,
"name": None,
"source": "recorder",
"display_unit_of_measurement": display_unit,
"statistics_unit_of_measurement": statistics_unit,
}
]
do_adhoc_statistics(hass, start=now)
await async_recorder_block_till_done(hass)
# Remove the state, statistics will now be fetched from the database
hass.states.async_remove("sensor.test")
await hass.async_block_till_done()
await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"})
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
{
"statistic_id": "sensor.test",
"has_mean": True,
"has_sum": False,
"name": None,
"source": "recorder",
"display_unit_of_measurement": display_unit,
"statistics_unit_of_measurement": statistics_unit,
}
]
await client.send_json(
{"id": 4, "type": "recorder/list_statistic_ids", "statistic_type": "dogs"}
)
response = await client.receive_json()
assert not response["success"]
await client.send_json(
{"id": 5, "type": "recorder/list_statistic_ids", "statistic_type": "mean"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == [
{
"statistic_id": "sensor.test",
"has_mean": True,
"has_sum": False,
"name": None,
"source": "recorder",
"display_unit_of_measurement": display_unit,
"statistics_unit_of_measurement": statistics_unit,
}
]
await client.send_json(
{"id": 6, "type": "recorder/list_statistic_ids", "statistic_type": "sum"}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == []
async def test_validate_statistics(hass, hass_ws_client, recorder_mock): async def test_validate_statistics(hass, hass_ws_client, recorder_mock):
"""Test validate_statistics can be called.""" """Test validate_statistics can be called."""
id = 1 id = 1
@ -83,7 +434,6 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock):
value = 10000 value = 10000
hass.config.units = units hass.config.units = units
await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {}) await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass) await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.test1", state, attributes=attributes) hass.states.async_set("sensor.test1", state, attributes=attributes)
@ -98,7 +448,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock):
await client.send_json( await client.send_json(
{ {
"id": 1, "id": 1,
"type": "history/statistics_during_period", "type": "recorder/statistics_during_period",
"start_time": now.isoformat(), "start_time": now.isoformat(),
"period": "5minute", "period": "5minute",
} }
@ -163,7 +513,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock):
await client.send_json( await client.send_json(
{ {
"id": 3, "id": 3,
"type": "history/statistics_during_period", "type": "recorder/statistics_during_period",
"start_time": now.isoformat(), "start_time": now.isoformat(),
"period": "5minute", "period": "5minute",
} }
@ -187,7 +537,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock):
await client.send_json( await client.send_json(
{ {
"id": 5, "id": 5,
"type": "history/statistics_during_period", "type": "recorder/statistics_during_period",
"start_time": now.isoformat(), "start_time": now.isoformat(),
"period": "5minute", "period": "5minute",
} }
@ -209,7 +559,6 @@ async def test_update_statistics_metadata(
state = 10 state = 10
hass.config.units = units hass.config.units = units
await async_setup_component(hass, "history", {})
await async_setup_component(hass, "sensor", {}) await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass) await async_recorder_block_till_done(hass)
hass.states.async_set("sensor.test", state, attributes=attributes) hass.states.async_set("sensor.test", state, attributes=attributes)
@ -220,7 +569,7 @@ async def test_update_statistics_metadata(
client = await hass_ws_client() client = await hass_ws_client()
await client.send_json({"id": 1, "type": "history/list_statistic_ids"}) await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert response["result"] == [ assert response["result"] == [
@ -247,7 +596,7 @@ async def test_update_statistics_metadata(
assert response["success"] assert response["success"]
await async_recorder_block_till_done(hass) await async_recorder_block_till_done(hass)
await client.send_json({"id": 3, "type": "history/list_statistic_ids"}) await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert response["result"] == [ assert response["result"] == [
@ -457,7 +806,6 @@ async def test_get_statistics_metadata(
now = dt_util.utcnow() now = dt_util.utcnow()
hass.config.units = units hass.config.units = units
await async_setup_component(hass, "history", {"history": {}})
await async_setup_component(hass, "sensor", {}) await async_setup_component(hass, "sensor", {})
await async_recorder_block_till_done(hass) await async_recorder_block_till_done(hass)