diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 31367cd0c9353..71200b93f3f14 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -12,7 +12,7 @@ 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 @@ async def get( "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 @@ async def get( 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 diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 053e006c13b11..93a5d27296525 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -27,6 +27,7 @@ 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"] diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index fa2332e71d777..46b5773bfaa16 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -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) diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index 92f3d61392c5b..7f3d8c76aed6a 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -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", } diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index fe202ef46bb79..f8d4ec7d9f759 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -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, + }