diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 77301532d3d..bae6c95507e 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -6,22 +6,22 @@ from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Literal, cast +from typing import cast from aiohttp import web import voluptuous as vol from homeassistant.components import frontend, websocket_api 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 ( Filters, 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.websocket_api import messages from homeassistant.core import HomeAssistant @@ -68,23 +68,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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( { 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 ) -> 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"), - ) - ) - - -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)) + _LOGGER.warning( + "WS API 'history/statistics_during_period' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" ) + await recorder_ws.ws_handle_get_statistics_during_period(hass, connection, msg) @websocket_api.websocket_command( @@ -152,14 +100,11 @@ async def ws_get_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"), - ) + _LOGGER.warning( + "WS API 'history/list_statistic_ids' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead" ) + await recorder_ws.ws_handle_list_statistic_ids(hass, connection, msg) def _ws_get_significant_states( diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 16813944780..70552bca67e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,13 +1,17 @@ """The Recorder websocket API.""" from __future__ import annotations +from datetime import datetime as dt import logging +from typing import Literal import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util from .const import MAX_QUEUE_BACKLOG @@ -15,6 +19,7 @@ from .statistics import ( async_add_external_statistics, async_import_statistics, list_statistic_ids, + statistics_during_period, validate_statistics, ) from .util import async_migration_in_progress, async_migration_is_live, get_instance @@ -25,15 +30,125 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) @callback def async_setup(hass: HomeAssistant) -> None: """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_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_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( diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 854ddb76191..5441722c9d7 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,24 +5,24 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel -from freezegun import freeze_time import pytest -from pytest import approx from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states 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 import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, - do_adhoc_statistics, 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" -POWER_SENSOR_ATTRIBUTES = { - "device_class": "power", - "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.""" +async def test_statistics_during_period(hass, hass_ws_client, recorder_mock, caplog): + """Test history/statistics_during_period forwards to recorder.""" now = dt_util.utcnow() - - hass.config.units = units 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() + + # Test the WS API works and issues a warning await client.send_json( { "id": 1, @@ -903,322 +865,53 @@ async def test_statistics_during_period( assert response["success"] assert response["result"] == {} - await client.send_json( - { - "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"] == { - "sensor.test": [ + assert ( + "WS API 'history/statistics_during_period' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" + ) in caplog.text + + # Test the WS API forwards to recorder + with patch( + "homeassistant.components.history.recorder_ws.ws_handle_get_statistics_during_period", + wraps=ws_handle_get_statistics_during_period, + ) as ws_mock: + await client.send_json( { - "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, + "id": 2, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", } - ] - } + ) + await client.receive_json() + ws_mock.assert_awaited_once() -@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 +async def test_list_statistic_ids(hass, hass_ws_client, recorder_mock, caplog): + """Test history/list_statistic_ids forwards to recorder.""" 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() - 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( - { - "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() + # Test the WS API works and issues a warning await client.send_json({"id": 1, "type": "history/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) + assert ( + "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"}) - 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": "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"] == [] + with patch( + "homeassistant.components.history.recorder_ws.ws_handle_list_statistic_ids", + wraps=ws_handle_list_statistic_ids, + ) as ws_mock: + await client.send_json({"id": 2, "type": "history/list_statistic_ids"}) + await client.receive_json() + ws_mock.assert_called_once() 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"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) client = await hass_ws_client() @@ -1358,8 +1050,6 @@ async def test_history_during_period_impossible_conditions( hass, hass_ws_client, recorder_mock ): """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, "sensor", {}) 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"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) 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"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) 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"}) await async_wait_recording_done(hass) - do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) client = await hass_ws_client() diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 269cebcba9f..cdec26be26d 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -4,6 +4,7 @@ from datetime import timedelta import threading from unittest.mock import patch +from freezegun import freeze_time import pytest from pytest import approx @@ -18,7 +19,7 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component 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 ( async_recorder_block_till_done, @@ -34,6 +35,11 @@ POWER_SENSOR_ATTRIBUTES = { "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", @@ -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): """Test validate_statistics can be called.""" id = 1 @@ -83,7 +434,6 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): value = 10000 hass.config.units = units - await async_setup_component(hass, "history", {}) await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) 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( { "id": 1, - "type": "history/statistics_during_period", + "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "period": "5minute", } @@ -163,7 +513,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): await client.send_json( { "id": 3, - "type": "history/statistics_during_period", + "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "period": "5minute", } @@ -187,7 +537,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): await client.send_json( { "id": 5, - "type": "history/statistics_during_period", + "type": "recorder/statistics_during_period", "start_time": now.isoformat(), "period": "5minute", } @@ -209,7 +559,6 @@ async def test_update_statistics_metadata( state = 10 hass.config.units = units - 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) @@ -220,7 +569,7 @@ async def test_update_statistics_metadata( 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() assert response["success"] assert response["result"] == [ @@ -247,7 +596,7 @@ async def test_update_statistics_metadata( assert response["success"] 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() assert response["success"] assert response["result"] == [ @@ -457,7 +806,6 @@ async def test_get_statistics_metadata( 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)