diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 22242432ff8..4770f8828b8 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -17,6 +17,7 @@ from onvif.client import ( from onvif.exceptions import ONVIFError from onvif.util import stringify_onvif_error import onvif_parsers +import onvif_parsers.util from zeep.exceptions import Fault, TransportError, ValidationError, XMLParseError from homeassistant.components import webhook @@ -196,7 +197,7 @@ class EventManager: topic = msg.Topic._value_1.rstrip("/.") # noqa: SLF001 try: - event = await onvif_parsers.parse(topic, unique_id, msg) + events = await onvif_parsers.parse(topic, unique_id, msg) error = None except onvif_parsers.errors.UnknownTopicError: if topic not in UNHANDLED_TOPICS: @@ -204,42 +205,43 @@ class EventManager: "%s: No registered handler for event from %s: %s", self.name, unique_id, - msg, + onvif_parsers.util.event_to_debug_format(msg), ) UNHANDLED_TOPICS.add(topic) continue except (AttributeError, KeyError) as e: - event = None + events = [] error = e - if not event: + if not events: LOGGER.warning( "%s: Unable to parse event from %s: %s: %s", self.name, unique_id, error, - msg, + onvif_parsers.util.event_to_debug_format(msg), ) continue - value = event.value - if event.device_class == "timestamp" and isinstance(value, str): - value = _local_datetime_or_none(value) + for event in events: + value = event.value + if event.device_class == "timestamp" and isinstance(value, str): + value = _local_datetime_or_none(value) - ha_event = Event( - uid=event.uid, - name=event.name, - platform=event.platform, - device_class=event.device_class, - unit_of_measurement=event.unit_of_measurement, - value=value, - entity_category=ENTITY_CATEGORY_MAPPING.get( - event.entity_category or "" - ), - entity_enabled=event.entity_enabled, - ) - self.get_uids_by_platform(ha_event.platform).add(ha_event.uid) - self._events[ha_event.uid] = ha_event + ha_event = Event( + uid=event.uid, + name=event.name, + platform=event.platform, + device_class=event.device_class, + unit_of_measurement=event.unit_of_measurement, + value=value, + entity_category=ENTITY_CATEGORY_MAPPING.get( + event.entity_category or "" + ), + entity_enabled=event.entity_enabled, + ) + self.get_uids_by_platform(ha_event.platform).add(ha_event.uid) + self._events[ha_event.uid] = ha_event def get_uid(self, uid: str) -> Event | None: """Retrieve event for given id.""" diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 5a097c525a3..9d15ca0afe6 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -15,7 +15,7 @@ "loggers": ["onvif", "wsdiscovery", "zeep"], "requirements": [ "onvif-zeep-async==4.0.4", - "onvif_parsers==1.2.2", + "onvif_parsers==2.3.0", "WSDiscovery==2.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 3febac865e3..0f263a31531 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1688,7 +1688,7 @@ onedrive-personal-sdk==0.1.7 onvif-zeep-async==4.0.4 # homeassistant.components.onvif -onvif_parsers==1.2.2 +onvif_parsers==2.3.0 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70c604089c1..c254d04296b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1474,7 +1474,7 @@ onedrive-personal-sdk==0.1.7 onvif-zeep-async==4.0.4 # homeassistant.components.onvif -onvif_parsers==1.2.2 +onvif_parsers==2.3.0 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 7c833ed0524..d31c84fcd45 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import collections from collections import defaultdict from unittest.mock import AsyncMock, MagicMock, patch @@ -196,7 +197,7 @@ async def setup_onvif_integration( source=config_entries.SOURCE_USER, capabilities=None, events=None, - raw_events: list[tuple[str, EventEntity]] | None = None, + raw_events: list[tuple[str, list[EventEntity]]] | None = None, ) -> tuple[MockConfigEntry, MagicMock, MagicMock]: """Create an ONVIF config entry.""" if not config: @@ -239,12 +240,14 @@ async def setup_onvif_integration( # to test the full parsing pipeline including conversions event_manager = EventManager(hass, mock_onvif_camera, config_entry, NAME) mock_messages = [] - event_by_topic: dict[str, EventEntity] = {} - for topic, raw_event in raw_events: + event_by_topic: collections.defaultdict[str, list[EventEntity]] = ( + collections.defaultdict(list) + ) + for topic, topic_events in raw_events: mock_msg = MagicMock() mock_msg.Topic._value_1 = topic mock_messages.append(mock_msg) - event_by_topic[topic] = raw_event + event_by_topic[topic].extend(topic_events) async def mock_parse(topic, unique_id, msg): return event_by_topic.get(topic) diff --git a/tests/components/onvif/test_event.py b/tests/components/onvif/test_event.py index eefc41c205f..d0d16d02368 100644 --- a/tests/components/onvif/test_event.py +++ b/tests/components/onvif/test_event.py @@ -95,13 +95,15 @@ async def test_timestamp_event_conversion(hass: HomeAssistant) -> None: raw_events=[ ( "tns1:Monitoring/LastReset", - EventEntity( - uid=LAST_RESET_UID, - name="Last Reset", - platform="sensor", - device_class="timestamp", - value="2023-10-01T12:00:00Z", - ), + [ + EventEntity( + uid=LAST_RESET_UID, + name="Last Reset", + platform="sensor", + device_class="timestamp", + value="2023-10-01T12:00:00Z", + ), + ], ), ], ) @@ -121,13 +123,15 @@ async def test_timestamp_event_invalid_value(hass: HomeAssistant) -> None: raw_events=[ ( "tns1:Monitoring/LastReset", - EventEntity( - uid=LAST_RESET_UID, - name="Last Reset", - platform="sensor", - device_class="timestamp", - value="0000-00-00T00:00:00Z", - ), + [ + EventEntity( + uid=LAST_RESET_UID, + name="Last Reset", + platform="sensor", + device_class="timestamp", + value="0000-00-00T00:00:00Z", + ), + ], ), ], ) @@ -135,3 +139,40 @@ async def test_timestamp_event_invalid_value(hass: HomeAssistant) -> None: state = hass.states.get("sensor.testcamera_last_reset") assert state is not None assert state.state == "unknown" + + +async def test_multiple_events_same_topic(hass: HomeAssistant) -> None: + """Test that multiple events with the same topic are all processed.""" + await setup_onvif_integration( + hass, + capabilities=Capabilities(events=True, imaging=True, ptz=True), + raw_events=[ + ( + "tns1:VideoSource/MotionAlarm", + [ + EventEntity( + uid=f"{MOTION_ALARM_UID}_1", + name="Motion Alarm 1", + platform="binary_sensor", + device_class="motion", + value=True, + ), + EventEntity( + uid=f"{MOTION_ALARM_UID}_2", + name="Motion Alarm 2", + platform="binary_sensor", + device_class="motion", + value=False, + ), + ], + ), + ], + ) + + state1 = hass.states.get("binary_sensor.testcamera_motion_alarm_1") + assert state1 is not None + assert state1.state == STATE_ON + + state2 = hass.states.get("binary_sensor.testcamera_motion_alarm_2") + assert state2 is not None + assert state2.state == STATE_OFF