Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions homeassistant/components/logbook/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# Domains that are always continuous
#
# These are hard coded here to avoid importing
# the entire counter and proximity integrations
# the entire counter, image, and proximity integrations
# to get the name of the domain.
ALWAYS_CONTINUOUS_DOMAINS = {"counter", "proximity"}
ALWAYS_CONTINUOUS_DOMAINS = {"counter", "image", "proximity"}
Comment thread
MartinHjelmare marked this conversation as resolved.

# Domains that are continuous if there is a UOM set on the entity
CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN}
Expand Down Expand Up @@ -47,4 +47,4 @@
AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED}

# Events that are built-in to the logbook or core
BUILT_IN_EVENTS = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE}
BUILT_IN_EVENTS = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE}
Comment thread
MartinHjelmare marked this conversation as resolved.
Outdated
92 changes: 90 additions & 2 deletions tests/components/logbook/test_websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2559,6 +2559,7 @@
entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"}
)
hass.states.async_set("counter.any", state)
hass.states.async_set("image.any", state)
hass.states.async_set("proximity.any", state)

# We will compare event subscriptions after closing the websocket connection,
Expand All @@ -2573,7 +2574,7 @@
"id": 7,
"type": "logbook/event_stream",
"start_time": now.isoformat(),
"entity_ids": ["sensor.uom", "counter.any", "proximity.any"],
"entity_ids": ["sensor.uom", "counter.any", "image.any", "proximity.any"],
}
)

Expand Down Expand Up @@ -2908,6 +2909,7 @@
entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"}
)
hass.states.async_set("counter.any", state)
hass.states.async_set("image.any", state)
hass.states.async_set("proximity.any", state)
hass.bus.async_fire("mock_event", {"device_id": device.id})
hass.bus.async_fire("mock_event", {"device_id": device2.id})
Expand All @@ -2924,7 +2926,7 @@
"id": 7,
"type": "logbook/event_stream",
"start_time": now.isoformat(),
"entity_ids": ["sensor.uom", "counter.any", "proximity.any"],
"entity_ids": ["sensor.uom", "counter.any", "image.any", "proximity.any"],
"device_ids": [device.id, device2.id],
}
)
Expand Down Expand Up @@ -3113,6 +3115,11 @@
{},
0, # Counter is an always continuous domain
),
(
"image.map0",
{},
0, # Image is an always continuous domain
),
(
"zone.home",
{},
Expand Down Expand Up @@ -3627,3 +3634,84 @@
assert len(heater_entries) == 1
assert heater_entries[0]["state"] == "on"
assert heater_entries[0]["context_user_id"] == user_id


@pytest.mark.usefixtures("recorder_mock")
async def test_image_entity_filtered_from_subscription(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test that image entities are filtered from logbook entity subscriptions.

Image entities use timestamps as state values and should be treated
as continuous domains to filter out redundant state change events.
This verifies the fix for https://github.com/home-assistant/core/issues/161039
where Roborock map image entities spammed Activity with frequent state changes.
"""
await asyncio.gather(
*[
async_setup_component(hass, comp, {})
for comp in ("homeassistant", "logbook")
]
)
await hass.async_block_till_done()

# Set up image entity and other entities for comparison
hass.states.async_set("light.kitchen", "on")
hass.states.async_set("image.roborock_map", "2026-01-01T00:00:00+00:00")
hass.states.async_set("sensor.temperature", "20.0")
await hass.async_block_till_done()

await async_wait_recording_done(hass)
now = dt_util.utcnow()
websocket_client = await hass_ws_client()
end_time = now + timedelta(hours=3)

# Subscribe to logbook events for these entities
await websocket_client.send_json(
{
"id": 1,
"type": "logbook/event_stream",
"start_time": now.isoformat(),
"end_time": end_time.isoformat(),
"entity_ids": ["light.kitchen", "image.roborock_map", "sensor.temperature"],
}
)

msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 1
assert msg["type"] == TYPE_RESULT
assert msg["success"]

# Drain historical backfill
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["event"]["partial"] is True

# Update image entity state (should be filtered as continuous)
hass.states.async_set(
"image.roborock_map", "2026-01-01T00:00:15+00:00"
)
await hass.async_block_till_done()

# Update light entity state (should NOT be filtered)
hass.states.async_set("light.kitchen", "off")
await hass.async_block_till_done()

# Receive events
msg = await asyncio.wait_for(websocket_client.receive_json(), 2)
assert msg["id"] == 1
assert msg["type"] == "event"

events = msg["event"]["events"]
# Image entity should be filtered out (continuous domain)
image_events = [e for e in events if e.get("entity_id") == "image.roborock_map"]
assert len(image_events) == 0, "Image entity state changes should be filtered"

# Light entity should appear (not a continuous domain)
light_events = [e for e in events if e.get("entity_id") == "light.kitchen"]
assert len(light_events) > 0, "Light entity state changes should appear"


def test_image_in_always_continuous_domains() -> None:
"""Test that image domain is in ALWAYS_CONTINUOUS_DOMAINS."""
from homeassistant.components.logbook.const import ALWAYS_CONTINUOUS_DOMAINS

Check failure on line 3716 in tests/components/logbook/test_websocket_api.py

View workflow job for this annotation

GitHub Actions / Run prek checks

ruff (PLC0415)

tests/components/logbook/test_websocket_api.py:3716:5: PLC0415 `import` should be at the top-level of a file
assert "image" in ALWAYS_CONTINUOUS_DOMAINS
Loading