Allow ONVIF devices to resume a PullPoint subscription when the camera reboots (#37711)

This commit is contained in:
Eric Severance 2020-08-11 14:53:30 -07:00 committed by GitHub
parent 4d1ef02802
commit 49b375ff94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 75 additions and 22 deletions

View File

@ -1,8 +1,13 @@
"""ONVIF event abstraction.""" """ONVIF event abstraction."""
import asyncio
import datetime as dt import datetime as dt
from typing import Callable, Dict, List, Optional, Set from typing import Callable, Dict, List, Optional, Set
from aiohttp.client_exceptions import ServerDisconnectedError from aiohttp.client_exceptions import (
ClientConnectorError,
ClientOSError,
ServerDisconnectedError,
)
from onvif import ONVIFCamera, ONVIFService from onvif import ONVIFCamera, ONVIFService
from zeep.exceptions import Fault from zeep.exceptions import Fault
@ -15,6 +20,13 @@ from .models import Event
from .parsers import PARSERS from .parsers import PARSERS
UNHANDLED_TOPICS = set() UNHANDLED_TOPICS = set()
SUBSCRIPTION_ERRORS = (
ClientConnectorError,
ClientOSError,
Fault,
ServerDisconnectedError,
asyncio.TimeoutError,
)
class EventManager: class EventManager:
@ -44,11 +56,7 @@ class EventManager:
"""Listen for data updates.""" """Listen for data updates."""
# This is the first listener, set up polling. # This is the first listener, set up polling.
if not self._listeners: if not self._listeners:
self._unsub_refresh = async_track_point_in_utc_time( self.schedule_pull()
self.hass,
self.async_pull_messages,
dt_util.utcnow() + dt.timedelta(seconds=1),
)
self._listeners.append(update_callback) self._listeners.append(update_callback)
@ -89,12 +97,14 @@ class EventManager:
) )
self.started = True self.started = True
return True
return self.started return False
async def async_stop(self) -> None: async def async_stop(self) -> None:
"""Unsubscribe from events.""" """Unsubscribe from events."""
self._listeners = [] self._listeners = []
self.started = False
if not self._subscription: if not self._subscription:
return return
@ -102,6 +112,40 @@ class EventManager:
await self._subscription.Unsubscribe() await self._subscription.Unsubscribe()
self._subscription = None self._subscription = None
async def async_restart(self, _now: dt = None) -> None:
"""Restart the subscription assuming the camera rebooted."""
if not self.started:
return
if self._subscription:
try:
await self._subscription.Unsubscribe()
except SUBSCRIPTION_ERRORS:
pass # Ignored. The subscription may no longer exist.
self._subscription = None
try:
restarted = await self.async_start()
except SUBSCRIPTION_ERRORS:
restarted = False
if not restarted:
LOGGER.warning(
"Failed to restart ONVIF PullPoint subscription for '%s'. Retrying...",
self.unique_id,
)
# Try again in a minute
self._unsub_refresh = async_track_point_in_utc_time(
self.hass,
self.async_restart,
dt_util.utcnow() + dt.timedelta(seconds=60),
)
elif self._listeners:
LOGGER.info(
"Restarted ONVIF PullPoint subscription for '%s'", self.unique_id
)
self.schedule_pull()
async def async_renew(self) -> None: async def async_renew(self) -> None:
"""Renew subscription.""" """Renew subscription."""
if not self._subscription: if not self._subscription:
@ -114,6 +158,14 @@ class EventManager:
) )
await self._subscription.Renew(termination_time) await self._subscription.Renew(termination_time)
def schedule_pull(self) -> None:
"""Schedule async_pull_messages to run."""
self._unsub_refresh = async_track_point_in_utc_time(
self.hass,
self.async_pull_messages,
dt_util.utcnow() + dt.timedelta(seconds=1),
)
async def async_pull_messages(self, _now: dt = None) -> None: async def async_pull_messages(self, _now: dt = None) -> None:
"""Pull messages from device.""" """Pull messages from device."""
if self.hass.state == CoreState.running: if self.hass.state == CoreState.running:
@ -129,26 +181,27 @@ class EventManager:
dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() dt_util.as_utc(response.TerminationTime) - dt_util.utcnow()
).total_seconds() < 7200: ).total_seconds() < 7200:
await self.async_renew() await self.async_renew()
except SUBSCRIPTION_ERRORS:
LOGGER.warning(
"Failed to fetch ONVIF PullPoint subscription messages for '%s'",
self.unique_id,
)
# Treat errors as if the camera restarted. Assume that the pullpoint
# subscription is no longer valid.
self._unsub_refresh = None
await self.async_restart()
return
# Parse response # Parse response
await self.async_parse_messages(response.NotificationMessage) await self.async_parse_messages(response.NotificationMessage)
except ServerDisconnectedError:
pass
except Fault:
pass
# Update entities # Update entities
for update_callback in self._listeners: for update_callback in self._listeners:
update_callback() update_callback()
# Reschedule another pull # Reschedule another pull
if self._listeners: if self._listeners:
self._unsub_refresh = async_track_point_in_utc_time( self.schedule_pull()
self.hass,
self.async_pull_messages,
dt_util.utcnow() + dt.timedelta(seconds=1),
)
# pylint: disable=protected-access # pylint: disable=protected-access
async def async_parse_messages(self, messages) -> None: async def async_parse_messages(self, messages) -> None:

View File

@ -2,7 +2,7 @@
"domain": "onvif", "domain": "onvif",
"name": "ONVIF", "name": "ONVIF",
"documentation": "https://www.home-assistant.io/integrations/onvif", "documentation": "https://www.home-assistant.io/integrations/onvif",
"requirements": ["onvif-zeep-async==0.4.0", "WSDiscovery==2.0.0"], "requirements": ["onvif-zeep-async==0.5.0", "WSDiscovery==2.0.0"],
"dependencies": ["ffmpeg"], "dependencies": ["ffmpeg"],
"codeowners": ["@hunterjm"], "codeowners": ["@hunterjm"],
"config_flow": true "config_flow": true

View File

@ -1005,7 +1005,7 @@ oemthermostat==1.1
onkyo-eiscp==1.2.7 onkyo-eiscp==1.2.7
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==0.4.0 onvif-zeep-async==0.5.0
# homeassistant.components.opengarage # homeassistant.components.opengarage
open-garage==0.1.4 open-garage==0.1.4

View File

@ -473,7 +473,7 @@ numpy==1.19.1
oauth2client==4.0.0 oauth2client==4.0.0
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==0.4.0 onvif-zeep-async==0.5.0
# homeassistant.components.openerz # homeassistant.components.openerz
openerz-api==0.1.0 openerz-api==0.1.0