Escape % and _ in history/logbook entity_globs, and use ? as _ (#72623)

Co-authored-by: pyos <pyos100500@gmail.com>
pull/72824/head
J. Nick Koston 2022-05-27 11:38:29 -10:00 committed by Paulus Schoutsen
parent 301f7647d1
commit c45dc49270
3 changed files with 223 additions and 7 deletions

View File

@ -18,8 +18,11 @@ DOMAIN = "history"
HISTORY_FILTERS = "history_filters"
GLOB_TO_SQL_CHARS = {
42: "%", # *
46: "_", # .
ord("*"): "%",
ord("?"): "_",
ord("%"): "\\%",
ord("_"): "\\_",
ord("\\"): "\\\\",
}
@ -122,7 +125,9 @@ def _globs_to_like(
) -> ClauseList:
"""Translate glob to sql."""
return or_(
cast(column, Text()).like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS)))
cast(column, Text()).like(
encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\"
)
for glob_str in glob_strs
for column in columns
)

View File

@ -719,7 +719,7 @@ async def test_fetch_period_api_with_entity_glob_exclude(
{
"history": {
"exclude": {
"entity_globs": ["light.k*"],
"entity_globs": ["light.k*", "binary_sensor.*_?"],
"domains": "switch",
"entities": "media_player.test",
},
@ -731,6 +731,9 @@ async def test_fetch_period_api_with_entity_glob_exclude(
hass.states.async_set("light.match", "on")
hass.states.async_set("switch.match", "on")
hass.states.async_set("media_player.test", "on")
hass.states.async_set("binary_sensor.sensor_l", "on")
hass.states.async_set("binary_sensor.sensor_r", "on")
hass.states.async_set("binary_sensor.sensor", "on")
await async_wait_recording_done(hass)
@ -740,9 +743,10 @@ async def test_fetch_period_api_with_entity_glob_exclude(
)
assert response.status == HTTPStatus.OK
response_json = await response.json()
assert len(response_json) == 2
assert response_json[0][0]["entity_id"] == "light.cow"
assert response_json[1][0]["entity_id"] == "light.match"
assert len(response_json) == 3
assert response_json[0][0]["entity_id"] == "binary_sensor.sensor"
assert response_json[1][0]["entity_id"] == "light.cow"
assert response_json[2][0]["entity_id"] == "light.match"
async def test_fetch_period_api_with_entity_glob_include_and_exclude(

View File

@ -22,6 +22,7 @@ from homeassistant.const import (
CONF_DOMAINS,
CONF_ENTITIES,
CONF_EXCLUDE,
CONF_INCLUDE,
EVENT_HOMEASSISTANT_START,
STATE_OFF,
STATE_ON,
@ -642,6 +643,212 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
assert sum(hass.bus.async_listeners().values()) == init_count
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
async def test_subscribe_unsubscribe_logbook_stream_included_entities(
hass, recorder_mock, hass_ws_client
):
"""Test subscribe/unsubscribe logbook stream with included entities."""
test_entities = (
"light.inc",
"switch.any",
"cover.included",
"cover.not_included",
"automation.not_included",
"binary_sensor.is_light",
)
now = dt_util.utcnow()
await asyncio.gather(
*[
async_setup_component(hass, comp, {})
for comp in ("homeassistant", "automation", "script")
]
)
await async_setup_component(
hass,
logbook.DOMAIN,
{
logbook.DOMAIN: {
CONF_INCLUDE: {
CONF_ENTITIES: ["light.inc"],
CONF_DOMAINS: ["switch"],
CONF_ENTITY_GLOBS: "*.included",
}
},
},
)
await hass.async_block_till_done()
init_count = sum(hass.bus.async_listeners().values())
for entity_id in test_entities:
hass.states.async_set(entity_id, STATE_ON)
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
await async_wait_recording_done(hass)
websocket_client = await hass_ws_client()
await websocket_client.send_json(
{"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()}
)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == TYPE_RESULT
assert msg["success"]
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert msg["event"]["events"] == [
{"entity_id": "light.inc", "state": "off", "when": ANY},
{"entity_id": "switch.any", "state": "off", "when": ANY},
{"entity_id": "cover.included", "state": "off", "when": ANY},
]
assert msg["event"]["start_time"] == now.timestamp()
assert msg["event"]["end_time"] > msg["event"]["start_time"]
assert msg["event"]["partial"] is True
for entity_id in test_entities:
hass.states.async_set(entity_id, STATE_ON)
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
hass.states.async_remove("light.zulu")
await hass.async_block_till_done()
hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"})
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert "partial" not in msg["event"]["events"]
assert msg["event"]["events"] == []
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 7
assert msg["type"] == "event"
assert "partial" not in msg["event"]["events"]
assert msg["event"]["events"] == [
{"entity_id": "light.inc", "state": "on", "when": ANY},
{"entity_id": "light.inc", "state": "off", "when": ANY},
{"entity_id": "switch.any", "state": "on", "when": ANY},
{"entity_id": "switch.any", "state": "off", "when": ANY},
{"entity_id": "cover.included", "state": "on", "when": ANY},
{"entity_id": "cover.included", "state": "off", "when": ANY},
]
for _ in range(3):
for entity_id in test_entities:
hass.states.async_set(entity_id, STATE_ON)
hass.states.async_set(entity_id, STATE_OFF)
await async_wait_recording_done(hass)
msg = await websocket_client.receive_json()
assert msg["id"] == 7
assert msg["type"] == "event"
assert msg["event"]["events"] == [
{"entity_id": "light.inc", "state": "on", "when": ANY},
{"entity_id": "light.inc", "state": "off", "when": ANY},
{"entity_id": "switch.any", "state": "on", "when": ANY},
{"entity_id": "switch.any", "state": "off", "when": ANY},
{"entity_id": "cover.included", "state": "on", "when": ANY},
{"entity_id": "cover.included", "state": "off", "when": ANY},
]
hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED,
{ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.included"},
)
hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED,
{ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"},
)
hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED,
{
ATTR_NAME: "Mock automation switch matching entity",
ATTR_ENTITY_ID: "switch.match_domain",
},
)
hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED,
{ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"},
)
hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED,
{ATTR_NAME: "Mock automation matches nothing"},
)
hass.bus.async_fire(
EVENT_AUTOMATION_TRIGGERED,
{ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.inc"},
)
await hass.async_block_till_done()
msg = await websocket_client.receive_json()
assert msg["id"] == 7
assert msg["type"] == "event"
assert msg["event"]["events"] == [
{
"context_id": ANY,
"domain": "automation",
"entity_id": "cover.included",
"message": "triggered",
"name": "Mock automation 3",
"source": None,
"when": ANY,
},
{
"context_id": ANY,
"domain": "automation",
"entity_id": "switch.match_domain",
"message": "triggered",
"name": "Mock automation switch matching entity",
"source": None,
"when": ANY,
},
{
"context_id": ANY,
"domain": "automation",
"entity_id": None,
"message": "triggered",
"name": "Mock automation switch matching domain",
"source": None,
"when": ANY,
},
{
"context_id": ANY,
"domain": "automation",
"entity_id": None,
"message": "triggered",
"name": "Mock automation matches nothing",
"source": None,
"when": ANY,
},
{
"context_id": ANY,
"domain": "automation",
"entity_id": "light.inc",
"message": "triggered",
"name": "Mock automation 3",
"source": None,
"when": ANY,
},
]
await websocket_client.send_json(
{"id": 8, "type": "unsubscribe_events", "subscription": 7}
)
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 8
assert msg["type"] == TYPE_RESULT
assert msg["success"]
# Check our listener got unsubscribed
assert sum(hass.bus.async_listeners().values()) == init_count
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
async def test_subscribe_unsubscribe_logbook_stream(
hass, recorder_mock, hass_ws_client