From 0d1e143c828c1dd2b9743c3b3689dee9a2c81320 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 13:01:31 -1000 Subject: [PATCH 1/8] Avoid selecting attributes when minimal_response is requested - Most history api requests do not need attributes and throw them away. We can avoid selecting them in the first place if we do not need them --- homeassistant/components/recorder/history.py | 65 +++++++++++++++----- tests/components/history/test_init.py | 36 +++++++++-- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index c4d15863a5bb71..22505e057a0bdc 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -6,8 +6,9 @@ import logging import time -from sqlalchemy import and_, bindparam, func +from sqlalchemy import Text, and_, bindparam, func from sqlalchemy.ext import baked +from sqlalchemy.sql.expression import literal from homeassistant.components import recorder from homeassistant.core import split_entity_id @@ -44,13 +45,20 @@ "water_heater", } -QUERY_STATES = [ +BASE_STATES = [ States.domain, States.entity_id, States.state, States.attributes, States.last_changed, States.last_updated, +] +QUERY_STATE_NO_ATTR = [ + *BASE_STATES, + literal(value=None, type_=Text).label("shared_attrs"), +] +QUERY_STATES = [ + *BASE_STATES, StateAttributes.shared_attrs, ] @@ -92,10 +100,16 @@ def get_significant_states_with_session( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) + need_attributes = ( + entity_ids is None + or not minimal_response + or any( + split_entity_id(ent_id)[0] in NEED_ATTRIBUTE_DOMAINS + for ent_id in entity_ids + ) ) + query_keys = QUERY_STATES if need_attributes else QUERY_STATE_NO_ATTR + baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) if significant_changes_only: baked_query += lambda q: q.filter( @@ -120,9 +134,10 @@ def get_significant_states_with_session( if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) + if need_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) states = execute( @@ -144,6 +159,7 @@ def get_significant_states_with_session( filters, include_start_time_state, minimal_response, + need_attributes, ) @@ -241,7 +257,13 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) def _get_states_with_session( - hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None + hass, + session, + utc_point_in_time, + entity_ids=None, + run=None, + filters=None, + need_attributes=True, ): """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: @@ -258,7 +280,8 @@ def _get_states_with_session( # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. - query = session.query(*QUERY_STATES) + query_keys = QUERY_STATES if need_attributes else QUERY_STATE_NO_ATTR + query = session.query(*query_keys) if entity_ids: # We got an include-list of entities, accelerate the query by filtering already @@ -278,9 +301,11 @@ def _get_states_with_session( query = query.join( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, - ).outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) + if need_attributes: + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) else: # We did not get an include-list of entities, query all states in the inner # query, then filter out unwanted domains as well as applying the custom filter. @@ -318,9 +343,10 @@ def _get_states_with_session( query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) if filters: query = filters.apply(query) - query = query.outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) + if need_attributes: + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) attr_cache = {} return [LazyState(row, attr_cache) for row in execute(query)] @@ -358,6 +384,7 @@ def _sorted_states_to_dict( filters=None, include_start_time_state=True, minimal_response=False, + need_attributes=True, ): """Convert SQL results into JSON friendly data structure. @@ -381,7 +408,13 @@ def _sorted_states_to_dict( if include_start_time_state: run = recorder.run_information_from_instance(hass, start_time) for state in _get_states_with_session( - hass, session, start_time, entity_ids, run=run, filters=filters + hass, + session, + start_time, + entity_ids, + run=run, + filters=filters, + need_attributes=need_attributes, ): state.last_changed = start_time state.last_updated = start_time diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 18fa3dc7625d58..f67c2d1d9e826a 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -17,8 +17,12 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import init_recorder_component -from tests.components.recorder.common import trigger_db_commit, wait_recording_done +from tests.common import async_init_recorder_component, init_recorder_component +from tests.components.recorder.common import ( + async_wait_recording_done_without_instance, + trigger_db_commit, + wait_recording_done, +) @pytest.mark.usefixtures("hass_history") @@ -604,14 +608,36 @@ async def test_fetch_period_api_with_use_include_order(hass, hass_client): async def test_fetch_period_api_with_minimal_response(hass, hass_client): """Test the fetch period view for history with minimal_response.""" - await hass.async_add_executor_job(init_recorder_component, hass) + await async_init_recorder_component(hass) + now = dt_util.utcnow() await async_setup_component(hass, "history", {}) - await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.states.async_set("sensor.power", 0, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) + hass.states.async_set("sensor.power", 50, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) + hass.states.async_set("sensor.power", 23, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) client = await hass_client() response = await client.get( - f"/api/history/period/{dt_util.utcnow().isoformat()}?minimal_response" + f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response" ) assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json[0]) == 3 + state_list = response_json[0] + + assert state_list[0]["entity_id"] == "sensor.power" + assert state_list[0]["attributes"] == {} + assert state_list[0]["state"] == "0" + + assert "attributes" not in state_list[1] + assert "entity_id" not in state_list[1] + assert state_list[1]["state"] == "50" + + assert state_list[2]["entity_id"] == "sensor.power" + assert state_list[2]["attributes"] == {} + assert state_list[2]["state"] == "23" async def test_fetch_period_api_with_no_timestamp(hass, hass_client): From 162e27ee8cc63ff62be114e1a5d1b678e4c175c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 14:24:51 -1000 Subject: [PATCH 2/8] Update tests/components/history/test_init.py --- tests/components/history/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index f67c2d1d9e826a..1590dcf1ed0d59 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -620,7 +620,7 @@ async def test_fetch_period_api_with_minimal_response(hass, hass_client): await async_wait_recording_done_without_instance(hass) client = await hass_client() response = await client.get( - f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response" + f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response&no_attributes" ) assert response.status == HTTPStatus.OK response_json = await response.json() From cb2bc251b10a96cf54bdda97f5e2aba06a38d41b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 16:02:44 -1000 Subject: [PATCH 3/8] naming --- homeassistant/components/history/__init__.py | 4 +++ homeassistant/components/recorder/history.py | 27 ++++++++------------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 5d870ffa8ee31a..5d4b2dc467554a 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -224,6 +224,7 @@ async def get( ) minimal_response = "minimal_response" in request.query + no_attributes = "no_attributes" in request.query hass = request.app["hass"] @@ -245,6 +246,7 @@ async def get( include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ), ) @@ -257,6 +259,7 @@ def _sorted_significant_states_json( include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ): """Fetch significant stats from the database as json.""" timer_start = time.perf_counter() @@ -272,6 +275,7 @@ def _sorted_significant_states_json( include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ) result = list(result.values()) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 22505e057a0bdc..b25d33f6c4ba74 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -86,6 +86,7 @@ def get_significant_states_with_session( include_start_time_state=True, significant_changes_only=True, minimal_response=False, + no_attributes=False, ): """ Return states changes during UTC period start_time - end_time. @@ -100,15 +101,7 @@ def get_significant_states_with_session( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - need_attributes = ( - entity_ids is None - or not minimal_response - or any( - split_entity_id(ent_id)[0] in NEED_ATTRIBUTE_DOMAINS - for ent_id in entity_ids - ) - ) - query_keys = QUERY_STATES if need_attributes else QUERY_STATE_NO_ATTR + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) if significant_changes_only: @@ -134,7 +127,7 @@ def get_significant_states_with_session( if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) - if need_attributes: + if not no_attributes: baked_query += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) @@ -159,7 +152,7 @@ def get_significant_states_with_session( filters, include_start_time_state, minimal_response, - need_attributes, + no_attributes, ) @@ -263,7 +256,7 @@ def _get_states_with_session( entity_ids=None, run=None, filters=None, - need_attributes=True, + no_attributes=False, ): """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: @@ -280,7 +273,7 @@ def _get_states_with_session( # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. - query_keys = QUERY_STATES if need_attributes else QUERY_STATE_NO_ATTR + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES query = session.query(*query_keys) if entity_ids: @@ -302,7 +295,7 @@ def _get_states_with_session( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, ) - if need_attributes: + if not no_attributes: query = query.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) @@ -343,7 +336,7 @@ def _get_states_with_session( query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) if filters: query = filters.apply(query) - if need_attributes: + if not no_attributes: query = query.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) @@ -384,7 +377,7 @@ def _sorted_states_to_dict( filters=None, include_start_time_state=True, minimal_response=False, - need_attributes=True, + no_attributes=False, ): """Convert SQL results into JSON friendly data structure. @@ -414,7 +407,7 @@ def _sorted_states_to_dict( entity_ids, run=run, filters=filters, - need_attributes=need_attributes, + no_attributes=no_attributes, ): state.last_changed = start_time state.last_updated = start_time From 7d3fc92a3eb2e37ebddd0c1d7a48d0faf13c65e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 21:56:02 -1000 Subject: [PATCH 4/8] make sure the apis history_stats uses can turn off attributes as well --- homeassistant/components/recorder/history.py | 46 +++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index b25d33f6c4ba74..d93ab40bdd7053 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -49,16 +49,17 @@ States.domain, States.entity_id, States.state, - States.attributes, States.last_changed, States.last_updated, ] QUERY_STATE_NO_ATTR = [ *BASE_STATES, + literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] QUERY_STATES = [ *BASE_STATES, + States.attributes, StateAttributes.shared_attrs, ] @@ -156,11 +157,14 @@ def get_significant_states_with_session( ) -def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): +def state_changes_during_period( + hass, start_time, end_time=None, entity_id=None, no_attributes=False +): """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) + lambda session: session.query(*query_keys) ) baked_query += lambda q: q.filter( @@ -177,9 +181,10 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) + if not no_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) states = execute( @@ -234,7 +239,14 @@ def get_last_state_changes(hass, number_of_states, entity_id): ) -def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): +def get_states( + hass, + utc_point_in_time, + entity_ids=None, + run=None, + filters=None, + no_attributes=False, +): """Return the states at a specific point in time.""" if run is None: run = recorder.run_information_from_instance(hass, utc_point_in_time) @@ -245,7 +257,7 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) with session_scope(hass=hass) as session: return _get_states_with_session( - hass, session, utc_point_in_time, entity_ids, run, filters + hass, session, utc_point_in_time, entity_ids, run, filters, no_attributes ) @@ -261,7 +273,7 @@ def _get_states_with_session( """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: return _get_single_entity_states_with_session( - hass, session, utc_point_in_time, entity_ids[0] + hass, session, utc_point_in_time, entity_ids[0], no_attributes ) if run is None: @@ -345,19 +357,21 @@ def _get_states_with_session( return [LazyState(row, attr_cache) for row in execute(query)] -def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): +def _get_single_entity_states_with_session( + hass, session, utc_point_in_time, entity_id, no_attributes=False +): # Use an entirely different (and extremely fast) query if we only # have a single entity id - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES + baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) baked_query += lambda q: q.filter( States.last_updated < bindparam("utc_point_in_time"), States.entity_id == bindparam("entity_id"), ) - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) + if not no_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.last_updated.desc()) baked_query += lambda q: q.limit(1) From 867fb4fc4d36ea15751fef643b9a4e4fa793d489 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 22:04:04 -1000 Subject: [PATCH 5/8] coverage --- homeassistant/components/recorder/history.py | 4 +- tests/components/recorder/test_history.py | 55 ++++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index d93ab40bdd7053..6a6fc80f8f0854 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -480,7 +480,7 @@ def _sorted_states_to_dict( return {key: val for key, val in result.items() if val} -def get_state(hass, utc_point_in_time, entity_id, run=None): +def get_state(hass, utc_point_in_time, entity_id, run=None, no_attributes=False): """Return a state at a specific point in time.""" - states = get_states(hass, utc_point_in_time, (entity_id,), run) + states = get_states(hass, utc_point_in_time, (entity_id,), run, None, no_attributes) return states[0] if states else None diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 67a666c934f0a0..79d701fcb2942a 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -15,11 +15,9 @@ from tests.components.recorder.common import wait_recording_done -def test_get_states(hass_recorder): - """Test getting states at a specific point in time.""" - hass = hass_recorder() +def _setup_get_states(hass): + """Set up for testing get_states.""" states = [] - now = dt_util.utcnow() with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): for i in range(5): @@ -48,6 +46,13 @@ def test_get_states(hass_recorder): wait_recording_done(hass) + return now, future, states + + +def test_get_states(hass_recorder): + """Test getting states at a specific point in time.""" + hass = hass_recorder() + now, future, states = _setup_get_states(hass) # Get states returns everything before POINT for all entities for state1, state2 in zip( states, @@ -75,6 +80,48 @@ def test_get_states(hass_recorder): assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None +def test_get_states_no_attributes(hass_recorder): + """Test getting states without attributes at a specific point in time.""" + hass = hass_recorder() + now, future, states = _setup_get_states(hass) + for state in states: + state.attributes = {} + + # Get states returns everything before POINT for all entities + for state1, state2 in zip( + states, + sorted( + history.get_states(hass, future, no_attributes=True), + key=lambda state: state.entity_id, + ), + ): + assert state1 == state2 + + # Get states returns everything before POINT for tested entities + entities = [f"test.point_in_time_{i % 5}" for i in range(5)] + for state1, state2 in zip( + states, + sorted( + history.get_states(hass, future, entities, no_attributes=True), + key=lambda state: state.entity_id, + ), + ): + assert state1 == state2 + + # Test get_state here because we have a DB setup + assert states[0] == history.get_state( + hass, future, states[0].entity_id, no_attributes=True + ) + + time_before_recorder_ran = now - timedelta(days=1000) + assert history.get_states(hass, time_before_recorder_ran, no_attributes=True) == [] + + assert ( + history.get_state(hass, time_before_recorder_ran, "demo.id", no_attributes=True) + is None + ) + + def test_state_changes_during_period(hass_recorder): """Test state change during period.""" hass = hass_recorder() From fa1b575d78399c6dce12199d58770841a678aa3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 22:08:05 -1000 Subject: [PATCH 6/8] coverage --- tests/components/recorder/test_history.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 79d701fcb2942a..e05eddd5e6ca89 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -5,6 +5,8 @@ import json from unittest.mock import patch, sentinel +import pytest + from homeassistant.components.recorder import history from homeassistant.components.recorder.models import process_timestamp import homeassistant.core as ha @@ -122,14 +124,21 @@ def test_get_states_no_attributes(hass_recorder): ) -def test_state_changes_during_period(hass_recorder): +@pytest.mark.parametrize( + "attributes, no_attributes", + [ + ({"attr": True}, False), + ({}, True), + ], +) +def test_state_changes_during_period(hass_recorder, attributes, no_attributes): """Test state change during period.""" hass = hass_recorder() entity_id = "media_player.test" def set_state(state): """Set the state.""" - hass.states.set(entity_id, state) + hass.states.set(entity_id, state, attributes) wait_recording_done(hass) return hass.states.get(entity_id) @@ -153,7 +162,9 @@ def set_state(state): set_state("Netflix") set_state("Plex") - hist = history.state_changes_during_period(hass, start, end, entity_id) + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes + ) assert states == hist[entity_id] From 1069d403d342c6686b1618599141eda78c2c5fc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 22:56:26 -1000 Subject: [PATCH 7/8] add params needed to support statistics sensors --- homeassistant/components/recorder/history.py | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 6a6fc80f8f0854..656ae5524f91d4 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import defaultdict +from datetime import datetime from itertools import groupby import logging import time @@ -11,7 +12,7 @@ from sqlalchemy.sql.expression import literal from homeassistant.components import recorder -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util from .models import ( @@ -158,8 +159,15 @@ def get_significant_states_with_session( def state_changes_during_period( - hass, start_time, end_time=None, entity_id=None, no_attributes=False -): + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + entity_id: str | None = None, + no_attributes: bool = False, + descending: bool = True, + limit: int | None = None, + include_start_time_state: bool = True, +) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES @@ -185,7 +193,12 @@ def state_changes_during_period( baked_query += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + + last_updated = States.last_updated.desc() if descending else States.last_updated + baked_query += lambda q: q.order_by(States.entity_id, last_updated) + + if limit: + baked_query += lambda q: q.limit(limit) states = execute( baked_query(session).params( @@ -195,7 +208,14 @@ def state_changes_during_period( entity_ids = [entity_id] if entity_id is not None else None - return _sorted_states_to_dict(hass, session, states, start_time, entity_ids) + return _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + include_start_time_state=include_start_time_state, + ) def get_last_state_changes(hass, number_of_states, entity_id): From e1b925db9499fa4ebb318c5ec2df06777b61a64b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 23:01:27 -1000 Subject: [PATCH 8/8] cover --- homeassistant/components/recorder/history.py | 2 +- tests/components/recorder/test_history.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 656ae5524f91d4..da128d69dc95bd 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -164,7 +164,7 @@ def state_changes_during_period( end_time: datetime | None = None, entity_id: str | None = None, no_attributes: bool = False, - descending: bool = True, + descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, ) -> dict[str, list[State]]: diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index e05eddd5e6ca89..5d1d72ca650133 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -125,13 +125,15 @@ def test_get_states_no_attributes(hass_recorder): @pytest.mark.parametrize( - "attributes, no_attributes", + "attributes, no_attributes, limit", [ - ({"attr": True}, False), - ({}, True), + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), ], ) -def test_state_changes_during_period(hass_recorder, attributes, no_attributes): +def test_state_changes_during_period(hass_recorder, attributes, no_attributes, limit): """Test state change during period.""" hass = hass_recorder() entity_id = "media_player.test" @@ -163,10 +165,10 @@ def set_state(state): set_state("Plex") hist = history.state_changes_during_period( - hass, start, end, entity_id, no_attributes + hass, start, end, entity_id, no_attributes, limit=limit ) - assert states == hist[entity_id] + assert states[:limit] == hist[entity_id] def test_get_last_state_changes(hass_recorder):