From 9649525fe6bf686f57d24b3bc386675a100cea04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Oct 2020 02:53:47 -0500 Subject: [PATCH] Increase coverage and optimize history skip_initial_state (#42014) Avoids a database query when all states have not changed in the time window when skip_initial_state is set. --- homeassistant/components/history/__init__.py | 25 +++++++++++-- tests/components/history/test_init.py | 38 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 79c60572ba5..a8ea18770d9 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,11 +1,11 @@ """Provide pre-made queries on top of the recorder component.""" from collections import defaultdict -from datetime import timedelta +from datetime import datetime as dt, timedelta from itertools import groupby import json import logging import time -from typing import Optional, cast +from typing import Iterable, Optional, cast from aiohttp import web from sqlalchemy import and_, bindparam, func, not_, or_ @@ -33,6 +33,7 @@ from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, ) +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -502,6 +503,13 @@ class HistoryPeriodView(HomeAssistantView): hass = request.app["hass"] + if ( + not include_start_time_state + and entity_ids + and not _entities_may_have_state_changes_after(hass, entity_ids, start_time) + ): + return self.json([]) + return cast( web.Response, await hass.async_add_executor_job( @@ -660,6 +668,19 @@ def _glob_to_like(glob_str): return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) +def _entities_may_have_state_changes_after( + hass: HomeAssistantType, entity_ids: Iterable, start_time: dt +) -> bool: + """Check the state machine to see if entities have changed since start time.""" + for entity_id in entity_ids: + state = hass.states.get(entity_id) + + if state is None or state.last_changed > start_time: + return True + + return False + + class LazyState(State): """A lazy version of core State.""" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index d1ca3b50869..0b35d5194fb 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -970,3 +970,41 @@ async def test_entity_ids_limit_via_api(hass, hass_client): assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_client): + """Test limiting history to entity_ids with skip_initial_state.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", + ) + assert response.status == 200 + response_json = await response.json() + assert len(response_json) == 0 + + when = dt_util.utcnow() - timedelta(minutes=1) + response = await client.get( + f"/api/history/period/{when.isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", + ) + assert response.status == 200 + response_json = await response.json() + assert len(response_json) == 2 + assert response_json[0][0]["entity_id"] == "light.kitchen" + assert response_json[1][0]["entity_id"] == "light.cow"