Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 49 additions & 16 deletions homeassistant/components/recorder/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
]

Expand Down Expand Up @@ -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
)
)
Comment thread
bdraco marked this conversation as resolved.
Outdated
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(
Expand All @@ -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(
Expand All @@ -144,6 +159,7 @@ def get_significant_states_with_session(
filters,
include_start_time_state,
minimal_response,
need_attributes,
)


Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand Down
36 changes: 31 additions & 5 deletions tests/components/history/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"
Comment thread
bdraco marked this conversation as resolved.
Outdated
)
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):
Expand Down