mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Escape % and _ in history/logbook entity_globs, and use ? as _ (#72623)
Co-authored-by: pyos <pyos100500@gmail.com>
This commit is contained in:
parent
9fe4aef4bc
commit
a43d47fa0b
@ -18,8 +18,11 @@ DOMAIN = "history"
|
|||||||
HISTORY_FILTERS = "history_filters"
|
HISTORY_FILTERS = "history_filters"
|
||||||
|
|
||||||
GLOB_TO_SQL_CHARS = {
|
GLOB_TO_SQL_CHARS = {
|
||||||
42: "%", # *
|
ord("*"): "%",
|
||||||
46: "_", # .
|
ord("?"): "_",
|
||||||
|
ord("%"): "\\%",
|
||||||
|
ord("_"): "\\_",
|
||||||
|
ord("\\"): "\\\\",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +125,9 @@ def _globs_to_like(
|
|||||||
) -> ClauseList:
|
) -> ClauseList:
|
||||||
"""Translate glob to sql."""
|
"""Translate glob to sql."""
|
||||||
return or_(
|
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 glob_str in glob_strs
|
||||||
for column in columns
|
for column in columns
|
||||||
)
|
)
|
||||||
|
@ -719,7 +719,7 @@ async def test_fetch_period_api_with_entity_glob_exclude(
|
|||||||
{
|
{
|
||||||
"history": {
|
"history": {
|
||||||
"exclude": {
|
"exclude": {
|
||||||
"entity_globs": ["light.k*"],
|
"entity_globs": ["light.k*", "binary_sensor.*_?"],
|
||||||
"domains": "switch",
|
"domains": "switch",
|
||||||
"entities": "media_player.test",
|
"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("light.match", "on")
|
||||||
hass.states.async_set("switch.match", "on")
|
hass.states.async_set("switch.match", "on")
|
||||||
hass.states.async_set("media_player.test", "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)
|
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
|
assert response.status == HTTPStatus.OK
|
||||||
response_json = await response.json()
|
response_json = await response.json()
|
||||||
assert len(response_json) == 2
|
assert len(response_json) == 3
|
||||||
assert response_json[0][0]["entity_id"] == "light.cow"
|
assert response_json[0][0]["entity_id"] == "binary_sensor.sensor"
|
||||||
assert response_json[1][0]["entity_id"] == "light.match"
|
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(
|
async def test_fetch_period_api_with_entity_glob_include_and_exclude(
|
||||||
|
@ -22,6 +22,7 @@ from homeassistant.const import (
|
|||||||
CONF_DOMAINS,
|
CONF_DOMAINS,
|
||||||
CONF_ENTITIES,
|
CONF_ENTITIES,
|
||||||
CONF_EXCLUDE,
|
CONF_EXCLUDE,
|
||||||
|
CONF_INCLUDE,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
@ -642,6 +643,212 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities(
|
|||||||
assert sum(hass.bus.async_listeners().values()) == init_count
|
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)
|
@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0)
|
||||||
async def test_subscribe_unsubscribe_logbook_stream(
|
async def test_subscribe_unsubscribe_logbook_stream(
|
||||||
hass, recorder_mock, hass_ws_client
|
hass, recorder_mock, hass_ws_client
|
||||||
|
Loading…
x
Reference in New Issue
Block a user