From 0b8fb36a7e6183309b621ef4f8bf00e7569e80ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Mar 2023 17:28:38 -1000 Subject: [PATCH] Fix onvif binary sensors (#90202) * Fix httpx client creating a new ssl context with each client While working on https://github.com/home-assistant/core/issues/83524 it was discovered that each new httpx client creates a new ssl context https://github.com/encode/httpx/blob/f1157dbc4102ac8e227a0a0bb12a877f592eff58/httpx/_transports/default.py#L261 If an ssl context is passed in creating a new one is avoided here https://github.com/encode/httpx/blob/f1157dbc4102ac8e227a0a0bb12a877f592eff58/httpx/_config.py#L110 This change makes httpx ssl no-verify behavior match aiohttp ssl no-verify behavior https://github.com/aio-libs/aiohttp/blob/6da04694fd87a39af9c3856048c9ff23ca815f88/aiohttp/connector.py#L892 aiohttp solved this by wrapping the code that generates the ssl context in an lru_cache * compact * Fix onvif binary sensors fixes #83524 needs https://github.com/hunterjm/python-onvif-zeep-async/pull/9 first to avoid recreating the memory leak * Fix memory leak in onvif Work around until https://github.com/hunterjm/python-onvif-zeep-async/pull/9 followup to https://github.com/home-assistant/core/pull/83006 * move check * onvif-zeep-async 1.2.2 * fix unloading --- homeassistant/components/onvif/event.py | 58 +++++++++++++++---------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 54c5b3b007b..84d75bf8048 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -27,6 +27,13 @@ SUBSCRIPTION_ERRORS = ( ) +def _stringify_onvif_error(error: Exception) -> str: + """Stringify ONVIF error.""" + if isinstance(error, Fault): + return error.message or str(error) or "Device sent empty error" + return str(error) + + class EventManager: """ONVIF Event Manager.""" @@ -79,30 +86,30 @@ class EventManager: async def async_start(self) -> bool: """Start polling events.""" - if await self.device.create_pullpoint_subscription(): - # Create subscription manager - self._subscription = self.device.create_subscription_service( - "PullPointSubscription" - ) + if not await self.device.create_pullpoint_subscription(): + return False - # Renew immediately - await self.async_renew() + # Create subscription manager + self._subscription = self.device.create_subscription_service( + "PullPointSubscription" + ) - # Initialize events - pullpoint = self.device.create_pullpoint_service() - with suppress(*SUBSCRIPTION_ERRORS): - await pullpoint.SetSynchronizationPoint() - response = await pullpoint.PullMessages( - {"MessageLimit": 100, "Timeout": dt.timedelta(seconds=5)} - ) + # Renew immediately + await self.async_renew() - # Parse event initialization - await self.async_parse_messages(response.NotificationMessage) + # Initialize events + pullpoint = self.device.create_pullpoint_service() + with suppress(*SUBSCRIPTION_ERRORS): + await pullpoint.SetSynchronizationPoint() + response = await pullpoint.PullMessages( + {"MessageLimit": 100, "Timeout": dt.timedelta(seconds=5)} + ) - self.started = True - return True + # Parse event initialization + await self.async_parse_messages(response.NotificationMessage) - return False + self.started = True + return True async def async_stop(self) -> None: """Unsubscribe from events.""" @@ -112,7 +119,8 @@ class EventManager: if not self._subscription: return - await self._subscription.Unsubscribe() + with suppress(*SUBSCRIPTION_ERRORS): + await self._subscription.Unsubscribe() self._subscription = None async def async_restart(self, _now: dt.datetime | None = None) -> None: @@ -148,7 +156,7 @@ class EventManager: "Retrying later: %s" ), self.unique_id, - err, + _stringify_onvif_error(err), ) if not restarted: @@ -170,7 +178,11 @@ class EventManager: .isoformat(timespec="seconds") .replace("+00:00", "Z") ) - await self._subscription.Renew(termination_time) + with suppress(*SUBSCRIPTION_ERRORS): + # The first time we renew, we may get a Fault error so we + # suppress it. The subscription will be restarted in + # async_restart later. + await self._subscription.Renew(termination_time) def async_schedule_pull(self) -> None: """Schedule async_pull_messages to run.""" @@ -203,7 +215,7 @@ class EventManager: " '%s': %s" ), self.unique_id, - err, + _stringify_onvif_error(err), ) # Treat errors as if the camera restarted. Assume that the pullpoint # subscription is no longer valid.