mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Implement use_include_order in the history websocket api (#71839)
This commit is contained in:
parent
cd2898886b
commit
68632cb267
@ -10,6 +10,8 @@ from typing import Any, Literal, cast
|
||||
|
||||
from aiohttp import web
|
||||
from sqlalchemy import not_, or_
|
||||
from sqlalchemy.ext.baked import BakedQuery
|
||||
from sqlalchemy.orm import Query
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import frontend, websocket_api
|
||||
@ -36,12 +38,12 @@ from homeassistant.helpers.entityfilter import (
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "history"
|
||||
HISTORY_FILTERS = "history_filters"
|
||||
HISTORY_USE_INCLUDE_ORDER = "history_use_include_order"
|
||||
|
||||
CONF_ORDER = "use_include_order"
|
||||
|
||||
GLOB_TO_SQL_CHARS = {
|
||||
@ -66,8 +68,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[HISTORY_FILTERS] = filters = sqlalchemy_filter_from_include_exclude_conf(
|
||||
conf
|
||||
)
|
||||
|
||||
use_include_order = conf.get(CONF_ORDER)
|
||||
hass.data[HISTORY_USE_INCLUDE_ORDER] = use_include_order = conf.get(CONF_ORDER)
|
||||
|
||||
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
|
||||
frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box")
|
||||
@ -176,30 +177,41 @@ def _ws_get_significant_states(
|
||||
hass: HomeAssistant,
|
||||
msg_id: int,
|
||||
start_time: dt,
|
||||
end_time: dt | None = None,
|
||||
entity_ids: list[str] | None = None,
|
||||
filters: Any | None = None,
|
||||
include_start_time_state: bool = True,
|
||||
significant_changes_only: bool = True,
|
||||
minimal_response: bool = False,
|
||||
no_attributes: bool = False,
|
||||
end_time: dt | None,
|
||||
entity_ids: list[str] | None,
|
||||
filters: Filters | None,
|
||||
use_include_order: bool | None,
|
||||
include_start_time_state: bool,
|
||||
significant_changes_only: bool,
|
||||
minimal_response: bool,
|
||||
no_attributes: bool,
|
||||
) -> str:
|
||||
"""Fetch history significant_states and convert them to json in the executor."""
|
||||
states = history.get_significant_states(
|
||||
hass,
|
||||
start_time,
|
||||
end_time,
|
||||
entity_ids,
|
||||
filters,
|
||||
include_start_time_state,
|
||||
significant_changes_only,
|
||||
minimal_response,
|
||||
no_attributes,
|
||||
True,
|
||||
)
|
||||
|
||||
if not use_include_order or not filters:
|
||||
return JSON_DUMP(messages.result_message(msg_id, states))
|
||||
|
||||
return JSON_DUMP(
|
||||
messages.result_message(
|
||||
msg_id,
|
||||
history.get_significant_states(
|
||||
hass,
|
||||
start_time,
|
||||
end_time,
|
||||
entity_ids,
|
||||
filters,
|
||||
include_start_time_state,
|
||||
significant_changes_only,
|
||||
minimal_response,
|
||||
no_attributes,
|
||||
True,
|
||||
),
|
||||
{
|
||||
order_entity: states.pop(order_entity)
|
||||
for order_entity in filters.included_entities
|
||||
if order_entity in states
|
||||
}
|
||||
| states,
|
||||
)
|
||||
)
|
||||
|
||||
@ -267,6 +279,7 @@ async def ws_get_history_during_period(
|
||||
end_time,
|
||||
entity_ids,
|
||||
hass.data[HISTORY_FILTERS],
|
||||
hass.data[HISTORY_USE_INCLUDE_ORDER],
|
||||
include_start_time_state,
|
||||
significant_changes_only,
|
||||
minimal_response,
|
||||
@ -351,20 +364,20 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
|
||||
def _sorted_significant_states_json(
|
||||
self,
|
||||
hass,
|
||||
start_time,
|
||||
end_time,
|
||||
entity_ids,
|
||||
include_start_time_state,
|
||||
significant_changes_only,
|
||||
minimal_response,
|
||||
no_attributes,
|
||||
):
|
||||
hass: HomeAssistant,
|
||||
start_time: dt,
|
||||
end_time: dt,
|
||||
entity_ids: list[str] | None,
|
||||
include_start_time_state: bool,
|
||||
significant_changes_only: bool,
|
||||
minimal_response: bool,
|
||||
no_attributes: bool,
|
||||
) -> web.Response:
|
||||
"""Fetch significant stats from the database as json."""
|
||||
timer_start = time.perf_counter()
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
result = history.get_significant_states_with_session(
|
||||
states = history.get_significant_states_with_session(
|
||||
hass,
|
||||
session,
|
||||
start_time,
|
||||
@ -377,25 +390,24 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
no_attributes,
|
||||
)
|
||||
|
||||
result = list(result.values())
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
elapsed = time.perf_counter() - timer_start
|
||||
_LOGGER.debug("Extracted %d states in %fs", sum(map(len, result)), elapsed)
|
||||
_LOGGER.debug(
|
||||
"Extracted %d states in %fs", sum(map(len, states.values())), elapsed
|
||||
)
|
||||
|
||||
# Optionally reorder the result to respect the ordering given
|
||||
# by any entities explicitly included in the configuration.
|
||||
if self.filters and self.use_include_order:
|
||||
sorted_result = []
|
||||
for order_entity in self.filters.included_entities:
|
||||
for state_list in result:
|
||||
if state_list[0].entity_id == order_entity:
|
||||
sorted_result.append(state_list)
|
||||
result.remove(state_list)
|
||||
break
|
||||
sorted_result.extend(result)
|
||||
result = sorted_result
|
||||
if not self.filters or not self.use_include_order:
|
||||
return self.json(list(states.values()))
|
||||
|
||||
return self.json(result)
|
||||
sorted_result = [
|
||||
states.pop(order_entity)
|
||||
for order_entity in self.filters.included_entities
|
||||
if order_entity in states
|
||||
]
|
||||
sorted_result.extend(list(states.values()))
|
||||
return self.json(sorted_result)
|
||||
|
||||
|
||||
def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None:
|
||||
@ -426,7 +438,7 @@ class Filters:
|
||||
self.included_domains: list[str] = []
|
||||
self.included_entity_globs: list[str] = []
|
||||
|
||||
def apply(self, query):
|
||||
def apply(self, query: Query) -> Query:
|
||||
"""Apply the entity filter."""
|
||||
if not self.has_config:
|
||||
return query
|
||||
@ -434,21 +446,18 @@ class Filters:
|
||||
return query.filter(self.entity_filter())
|
||||
|
||||
@property
|
||||
def has_config(self):
|
||||
def has_config(self) -> bool:
|
||||
"""Determine if there is any filter configuration."""
|
||||
if (
|
||||
return bool(
|
||||
self.excluded_entities
|
||||
or self.excluded_domains
|
||||
or self.excluded_entity_globs
|
||||
or self.included_entities
|
||||
or self.included_domains
|
||||
or self.included_entity_globs
|
||||
):
|
||||
return True
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def bake(self, baked_query):
|
||||
def bake(self, baked_query: BakedQuery) -> None:
|
||||
"""Update a baked query.
|
||||
|
||||
Works the same as apply on a baked_query.
|
||||
@ -458,7 +467,7 @@ class Filters:
|
||||
|
||||
baked_query += lambda q: q.filter(self.entity_filter())
|
||||
|
||||
def entity_filter(self):
|
||||
def entity_filter(self) -> Any:
|
||||
"""Generate the entity filter query."""
|
||||
includes = []
|
||||
if self.included_domains:
|
||||
@ -502,7 +511,7 @@ class Filters:
|
||||
return or_(*includes) & not_(or_(*excludes))
|
||||
|
||||
|
||||
def _glob_to_like(glob_str):
|
||||
def _glob_to_like(glob_str: str) -> Any:
|
||||
"""Translate glob to sql."""
|
||||
return history_models.States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS))
|
||||
|
||||
|
@ -83,7 +83,7 @@ def statement_for_request(
|
||||
# No entities: logbook sends everything for the timeframe
|
||||
# limited by the context_id and the yaml configured filter
|
||||
if not entity_ids:
|
||||
entity_filter = filters.entity_filter() if filters else None # type: ignore[no-untyped-call]
|
||||
entity_filter = filters.entity_filter() if filters else None
|
||||
return _all_stmt(start_day, end_day, event_types, entity_filter, context_id)
|
||||
|
||||
# Multiple entities: logbook sends everything for the timeframe for the entities
|
||||
|
@ -11,6 +11,7 @@ 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.const import CONF_DOMAINS, CONF_ENTITIES
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.setup import async_setup_component
|
||||
@ -1476,3 +1477,60 @@ async def test_history_during_period_bad_end_time(hass, hass_ws_client, recorder
|
||||
response = await client.receive_json()
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == "invalid_end_time"
|
||||
|
||||
|
||||
async def test_history_during_period_with_use_include_order(
|
||||
hass, hass_ws_client, recorder_mock
|
||||
):
|
||||
"""Test history_during_period."""
|
||||
now = dt_util.utcnow()
|
||||
sort_order = ["sensor.two", "sensor.four", "sensor.one"]
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"history",
|
||||
{
|
||||
history.DOMAIN: {
|
||||
history.CONF_ORDER: True,
|
||||
history.CONF_INCLUDE: {
|
||||
CONF_ENTITIES: sort_order,
|
||||
CONF_DOMAINS: ["sensor"],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
await async_setup_component(hass, "sensor", {})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.one", "on", attributes={"any": "attr"})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.two", "off", attributes={"any": "attr"})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.three", "off", attributes={"any": "changed"})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.four", "off", attributes={"any": "again"})
|
||||
await async_recorder_block_till_done(hass)
|
||||
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()
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "history/history_during_period",
|
||||
"start_time": now.isoformat(),
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
"minimal_response": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["id"] == 1
|
||||
|
||||
assert list(response["result"]) == [
|
||||
*sort_order,
|
||||
"sensor.three",
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user