History API entity_id validation (#90067)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
f5911bcad6
commit
bf4559719a
5 changed files with 412 additions and 5 deletions
|
@ -12,7 +12,7 @@ from homeassistant.components import frontend
|
|||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.components.recorder.util import session_scope
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, valid_entity_id
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
@ -73,6 +73,14 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
"filter_entity_id is missing", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
hass = request.app["hass"]
|
||||
|
||||
for entity_id in entity_ids:
|
||||
if not hass.states.get(entity_id) and not valid_entity_id(entity_id):
|
||||
return self.json_message(
|
||||
"Invalid filter_entity_id", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
now = dt_util.utcnow()
|
||||
if datetime_:
|
||||
start_time = dt_util.as_utc(datetime_)
|
||||
|
@ -96,8 +104,6 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
minimal_response = "minimal_response" in request.query
|
||||
no_attributes = "no_attributes" in request.query
|
||||
|
||||
hass = request.app["hass"]
|
||||
|
||||
if (
|
||||
not include_start_time_state
|
||||
and entity_ids
|
||||
|
|
|
@ -27,6 +27,7 @@ from homeassistant.core import (
|
|||
State,
|
||||
callback,
|
||||
is_callback,
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time,
|
||||
|
@ -95,7 +96,7 @@ def _ws_get_significant_states(
|
|||
vol.Required("type"): "history/history_during_period",
|
||||
vol.Required("start_time"): str,
|
||||
vol.Optional("end_time"): str,
|
||||
vol.Optional("entity_ids"): [str],
|
||||
vol.Required("entity_ids"): [str],
|
||||
vol.Optional("include_start_time_state", default=True): bool,
|
||||
vol.Optional("significant_changes_only", default=True): bool,
|
||||
vol.Optional("minimal_response", default=False): bool,
|
||||
|
@ -129,7 +130,12 @@ async def ws_get_history_during_period(
|
|||
connection.send_result(msg["id"], {})
|
||||
return
|
||||
|
||||
entity_ids = msg.get("entity_ids")
|
||||
entity_ids: list[str] = msg["entity_ids"]
|
||||
for entity_id in entity_ids:
|
||||
if not hass.states.get(entity_id) and not valid_entity_id(entity_id):
|
||||
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
|
||||
return
|
||||
|
||||
include_start_time_state = msg["include_start_time_state"]
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
||||
|
@ -428,6 +434,11 @@ async def ws_stream(
|
|||
return
|
||||
|
||||
entity_ids: list[str] = msg["entity_ids"]
|
||||
for entity_id in entity_ids:
|
||||
if not hass.states.get(entity_id) and not valid_entity_id(entity_id):
|
||||
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
|
||||
return
|
||||
|
||||
include_start_time_state = msg["include_start_time_state"]
|
||||
significant_changes_only = msg["significant_changes_only"]
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
|
|
@ -696,3 +696,72 @@ async def test_fetch_period_api_with_no_entity_ids(
|
|||
assert response.status == HTTPStatus.BAD_REQUEST
|
||||
response_json = await response.json()
|
||||
assert response_json == {"message": "filter_entity_id is missing"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("filter_entity_id", "status_code", "response_contains1", "response_contains2"),
|
||||
[
|
||||
("light.kitchen,light.cow", HTTPStatus.OK, "light.kitchen", "light.cow"),
|
||||
(
|
||||
"light.kitchen,light.cow&",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"message",
|
||||
"Invalid filter_entity_id",
|
||||
),
|
||||
(
|
||||
"light.kitchen,li-ght.cow",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"message",
|
||||
"Invalid filter_entity_id",
|
||||
),
|
||||
(
|
||||
"light.kit!chen",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"message",
|
||||
"Invalid filter_entity_id",
|
||||
),
|
||||
(
|
||||
"lig+ht.kitchen,light.cow",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"message",
|
||||
"Invalid filter_entity_id",
|
||||
),
|
||||
(
|
||||
"light.kitchenlight.cow",
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
"message",
|
||||
"Invalid filter_entity_id",
|
||||
),
|
||||
("cow", HTTPStatus.BAD_REQUEST, "message", "Invalid filter_entity_id"),
|
||||
],
|
||||
)
|
||||
async def test_history_with_invalid_entity_ids(
|
||||
filter_entity_id,
|
||||
status_code,
|
||||
response_contains1,
|
||||
response_contains2,
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test sending valid and invalid entity_ids to the API."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"history",
|
||||
{"history": {}},
|
||||
)
|
||||
hass.states.async_set("light.kitchen", "on")
|
||||
hass.states.async_set("light.cow", "on")
|
||||
|
||||
await async_wait_recording_done(hass)
|
||||
now = dt_util.utcnow().isoformat()
|
||||
client = await hass_client()
|
||||
|
||||
response = await client.get(
|
||||
f"/api/history/period/{now}",
|
||||
params={"filter_entity_id": filter_entity_id},
|
||||
)
|
||||
assert response.status == status_code
|
||||
response_json = await response.json()
|
||||
assert response_contains1 in str(response_json)
|
||||
assert response_contains2 in str(response_json)
|
||||
|
|
|
@ -978,6 +978,7 @@ async def test_history_during_period_bad_start_time(
|
|||
{
|
||||
"id": 1,
|
||||
"type": "history/history_during_period",
|
||||
"entity_ids": ["sensor.pet"],
|
||||
"start_time": "cats",
|
||||
}
|
||||
)
|
||||
|
@ -1004,6 +1005,7 @@ async def test_history_during_period_bad_end_time(
|
|||
{
|
||||
"id": 1,
|
||||
"type": "history/history_during_period",
|
||||
"entity_ids": ["sensor.pet"],
|
||||
"start_time": now.isoformat(),
|
||||
"end_time": "dogs",
|
||||
}
|
||||
|
|
|
@ -417,6 +417,7 @@ async def test_history_during_period_bad_start_time(
|
|||
{
|
||||
"id": 1,
|
||||
"type": "history/history_during_period",
|
||||
"entity_ids": ["sensor.pet"],
|
||||
"start_time": "cats",
|
||||
}
|
||||
)
|
||||
|
@ -442,6 +443,7 @@ async def test_history_during_period_bad_end_time(
|
|||
{
|
||||
"id": 1,
|
||||
"type": "history/history_during_period",
|
||||
"entity_ids": ["sensor.pet"],
|
||||
"start_time": now.isoformat(),
|
||||
"end_time": "dogs",
|
||||
}
|
||||
|
@ -1524,3 +1526,320 @@ async def test_overflow_queue(
|
|||
assert listeners_without_writes(
|
||||
hass.bus.async_listeners()
|
||||
) == listeners_without_writes(init_listeners)
|
||||
|
||||
|
||||
async def test_history_during_period_for_invalid_entity_ids(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test history_during_period for valid and invalid entity ids."""
|
||||
now = dt_util.utcnow()
|
||||
|
||||
await async_setup_component(hass, "history", {})
|
||||
await async_setup_component(hass, "sensor", {})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.one", "on", attributes={"any": "attr"})
|
||||
sensor_one_last_updated = hass.states.get("sensor.one").last_updated
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.two", "off", attributes={"any": "attr"})
|
||||
sensor_two_last_updated = hass.states.get("sensor.two").last_updated
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.three", "off", attributes={"any": "again"})
|
||||
await async_recorder_block_till_done(hass)
|
||||
await async_wait_recording_done(hass)
|
||||
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(),
|
||||
"entity_ids": ["sensor.one"],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response == {
|
||||
"result": {
|
||||
"sensor.one": [
|
||||
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
|
||||
],
|
||||
},
|
||||
"id": 1,
|
||||
"type": "result",
|
||||
"success": True,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "history/history_during_period",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sensor.one", "sensor.two"],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response == {
|
||||
"result": {
|
||||
"sensor.one": [
|
||||
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
|
||||
],
|
||||
"sensor.two": [
|
||||
{"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"}
|
||||
],
|
||||
},
|
||||
"id": 2,
|
||||
"type": "result",
|
||||
"success": True,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 3,
|
||||
"type": "history/history_during_period",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sens!or.one", "two"],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"] is False
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_entity_ids",
|
||||
"message": "Invalid entity_ids",
|
||||
},
|
||||
"id": 3,
|
||||
"type": "result",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 4,
|
||||
"type": "history/history_during_period",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sensor.one", "sensortwo."],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"] is False
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_entity_ids",
|
||||
"message": "Invalid entity_ids",
|
||||
},
|
||||
"id": 4,
|
||||
"type": "result",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "history/history_during_period",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["one", ".sensortwo"],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"] is False
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_entity_ids",
|
||||
"message": "Invalid entity_ids",
|
||||
},
|
||||
"id": 5,
|
||||
"type": "result",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
|
||||
async def test_history_stream_for_invalid_entity_ids(
|
||||
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||
) -> None:
|
||||
"""Test history stream for invalid and valid entity ids."""
|
||||
|
||||
now = dt_util.utcnow()
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"history",
|
||||
{history.DOMAIN: {}},
|
||||
)
|
||||
|
||||
await async_setup_component(hass, "sensor", {})
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.one", "on", attributes={"any": "attr"})
|
||||
sensor_one_last_updated = hass.states.get("sensor.one").last_updated
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.two", "off", attributes={"any": "attr"})
|
||||
sensor_two_last_updated = hass.states.get("sensor.two").last_updated
|
||||
await async_recorder_block_till_done(hass)
|
||||
hass.states.async_set("sensor.three", "off", attributes={"any": "again"})
|
||||
await async_recorder_block_till_done(hass)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
client = await hass_ws_client()
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "history/stream",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sensor.one"],
|
||||
"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 response["type"] == "result"
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response == {
|
||||
"event": {
|
||||
"end_time": sensor_one_last_updated.timestamp(),
|
||||
"start_time": now.timestamp(),
|
||||
"states": {
|
||||
"sensor.one": [
|
||||
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
|
||||
],
|
||||
},
|
||||
},
|
||||
"id": 1,
|
||||
"type": "event",
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "history/stream",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sensor.one", "sensor.two"],
|
||||
"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"] == 2
|
||||
assert response["type"] == "result"
|
||||
|
||||
response = await client.receive_json()
|
||||
assert response == {
|
||||
"event": {
|
||||
"end_time": sensor_two_last_updated.timestamp(),
|
||||
"start_time": now.timestamp(),
|
||||
"states": {
|
||||
"sensor.one": [
|
||||
{"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"}
|
||||
],
|
||||
"sensor.two": [
|
||||
{"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"}
|
||||
],
|
||||
},
|
||||
},
|
||||
"id": 2,
|
||||
"type": "event",
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 3,
|
||||
"type": "history/stream",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sens!or.one", "two"],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
"minimal_response": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"] is False
|
||||
assert response["id"] == 3
|
||||
assert response["type"] == "result"
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_entity_ids",
|
||||
"message": "Invalid entity_ids",
|
||||
},
|
||||
"id": 3,
|
||||
"type": "result",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 4,
|
||||
"type": "history/stream",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["sensor.one", "sensortwo."],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
"minimal_response": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"] is False
|
||||
assert response["id"] == 4
|
||||
assert response["type"] == "result"
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_entity_ids",
|
||||
"message": "Invalid entity_ids",
|
||||
},
|
||||
"id": 4,
|
||||
"type": "result",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "history/stream",
|
||||
"start_time": now.isoformat(),
|
||||
"entity_ids": ["one", ".sensortwo"],
|
||||
"include_start_time_state": True,
|
||||
"significant_changes_only": False,
|
||||
"no_attributes": True,
|
||||
"minimal_response": True,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["success"] is False
|
||||
assert response["id"] == 5
|
||||
assert response["type"] == "result"
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_entity_ids",
|
||||
"message": "Invalid entity_ids",
|
||||
},
|
||||
"id": 5,
|
||||
"type": "result",
|
||||
"success": False,
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue