From 8fc30569a9d201c96efc12ed3893d5b1a06a14a0 Mon Sep 17 00:00:00 2001 From: Geert van Horrik Date: Fri, 3 May 2019 19:01:12 +0200 Subject: [PATCH] Fix bad request for some IP ONVIF camera (#22972) * Onvif camera improvements using zeep * Fix static code checks * Make obtain_input_uri async * Convert several methods to async * Fix static checks * Fix static checks * Fix requirements_all.txt * Lint improvements * Async services * Use onvif-zeep-async and check if PTZ service is available before creating it * Remove some hacks that are now defined in onvif-zeep-async * Don't log input, it might contain sensitive information * Static code analysis fixes * Run requirements stuff * Fix * Remove suds requirement * Onvif camera improvements using zeep * Fix static code checks * Make obtain_input_uri async * Convert several methods to async * Fix static checks * Fix static checks * Fix requirements_all.txt * Lint improvements * Async services * Use onvif-zeep-async and check if PTZ service is available before creating it * Remove some hacks that are now defined in onvif-zeep-async * Don't log input, it might contain sensitive information * Static code analysis fixes * Run requirements stuff * Fix * Remove suds requirement * Use dt_util.utcnow * Platform setup should not have a return value * Remove explicit dependency to zeep[async] * Bump onvif-zeep-async to 0.1.2 * Update requirements_all.txt * Add exception handling * Fix static checks * Don't catch generic exceptions * Update camera.py --- homeassistant/components/onvif/camera.py | 192 +++++++++++++++---- homeassistant/components/onvif/manifest.json | 6 +- requirements_all.txt | 8 +- 3 files changed, 154 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6a773a854c9..ea3d0277136 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,8 +1,13 @@ -"""Support for ONVIF Cameras with FFmpeg as decoder.""" +""" +Support for ONVIF Cameras with FFmpeg as decoder. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.onvif/ +""" import asyncio +import datetime as dt import logging import os - import voluptuous as vol from homeassistant.const import ( @@ -65,9 +70,12 @@ SERVICE_PTZ_SCHEMA = vol.Schema({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a ONVIF camera.""" - def handle_ptz(service): + _LOGGER.debug("Setting up the ONVIF camera platform") + + async def async_handle_ptz(service): """Handle PTZ service call.""" pan = service.data.get(ATTR_PAN, None) tilt = service.data.get(ATTR_TILT, None) @@ -81,20 +89,34 @@ def setup_platform(hass, config, add_entities, discovery_info=None): target_cameras = [camera for camera in all_cameras if camera.entity_id in entity_ids] for camera in target_cameras: - camera.perform_ptz(pan, tilt, zoom) + await camera.async_perform_ptz(pan, tilt, zoom) - hass.services.register(DOMAIN, SERVICE_PTZ, handle_ptz, - schema=SERVICE_PTZ_SCHEMA) - add_entities([ONVIFHassCamera(hass, config)]) + hass.services.async_register(DOMAIN, SERVICE_PTZ, async_handle_ptz, + schema=SERVICE_PTZ_SCHEMA) + + _LOGGER.debug("Constructing the ONVIFHassCamera") + + hass_camera = ONVIFHassCamera(hass, config) + + await hass_camera.async_initialize() + + async_add_entities([hass_camera]) + return class ONVIFHassCamera(Camera): """An implementation of an ONVIF camera.""" def __init__(self, hass, config): - """Initialize a ONVIF camera.""" + """Initialize an ONVIF camera.""" super().__init__() + + _LOGGER.debug("Importing dependencies") + import onvif + from onvif import ONVIFCamera + + _LOGGER.debug("Setting up the ONVIF camera component") self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) @@ -103,29 +125,105 @@ class ONVIFHassCamera(Camera): self._name = config.get(CONF_NAME) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._profile_index = config.get(CONF_PROFILE) + self._ptz_service = None self._input = None - self._media_service = \ - onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( - self._host, self._port), - self._username, self._password, - '{}/wsdl/media.wsdl'.format(os.path.dirname( - onvif.__file__))) - self._ptz_service = \ - onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( - self._host, self._port), - self._username, self._password, - '{}/wsdl/ptz.wsdl'.format(os.path.dirname( - onvif.__file__))) + _LOGGER.debug("Setting up the ONVIF camera device @ '%s:%s'", + self._host, + self._port) - def obtain_input_uri(self): + self._camera = ONVIFCamera(self._host, + self._port, + self._username, + self._password, + '{}/wsdl/' + .format(os.path.dirname(onvif.__file__))) + + async def async_initialize(self): + """ + Initialize the camera. + + Initializes the camera by obtaining the input uri and connecting to + the camera. Also retrieves the ONVIF profiles. + """ + from aiohttp.client_exceptions import ClientConnectorError + from homeassistant.exceptions import PlatformNotReady + from zeep.exceptions import Fault + import homeassistant.util.dt as dt_util + + try: + _LOGGER.debug("Updating service addresses") + + await self._camera.update_xaddrs() + + _LOGGER.debug("Setting up the ONVIF device management service") + + devicemgmt = self._camera.create_devicemgmt_service() + + _LOGGER.debug("Retrieving current camera date/time") + + system_date = dt_util.utcnow() + device_time = await devicemgmt.GetSystemDateAndTime() + cdate = device_time.UTCDateTime + cam_date = dt.datetime(cdate.Date.Year, cdate.Date.Month, + cdate.Date.Day, cdate.Time.Hour, + cdate.Time.Minute, cdate.Time.Second, + 0, dt_util.UTC) + + _LOGGER.debug("Camera date/time: %s", + cam_date) + + _LOGGER.debug("System date/time: %s", + system_date) + + dt_diff = cam_date - system_date + dt_diff_seconds = dt_diff.total_seconds() + + if dt_diff_seconds > 5: + _LOGGER.warning("The date/time on the camera is '%s', " + "which is different from the system '%s', " + "this could lead to authentication issues", + cam_date, + system_date) + + _LOGGER.debug("Obtaining input uri") + + await self.async_obtain_input_uri() + + _LOGGER.debug("Setting up the ONVIF PTZ service") + + if self._camera.get_service('ptz', create=False) is None: + _LOGGER.warning("PTZ is not available on this camera") + else: + self._ptz_service = self._camera.create_ptz_service() + _LOGGER.debug("Completed set up of the ONVIF camera component") + except ClientConnectorError as err: + _LOGGER.warning("Couldn't connect to camera '%s', but will " + "retry later. Error: %s", + self._name, err) + raise PlatformNotReady + except Fault as err: + _LOGGER.error("Couldn't connect to camera '%s', please verify " + "that the credentials are correct. Error: %s", + self._name, err) + return + + async def async_obtain_input_uri(self): """Set the input uri for the camera.""" from onvif import exceptions + _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", self._host, self._port) try: - profiles = self._media_service.GetProfiles() + _LOGGER.debug("Retrieving profiles") + + media_service = self._camera.create_media_service() + + profiles = await media_service.GetProfiles() + + _LOGGER.debug("Retrieved '%d' profiles", + len(profiles)) if self._profile_index >= len(profiles): _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d." @@ -133,29 +231,41 @@ class ONVIFHassCamera(Camera): self._name, self._profile_index) self._profile_index = -1 - req = self._media_service.create_type('GetStreamUri') + _LOGGER.debug("Using profile index '%d'", + self._profile_index) - # pylint: disable=protected-access - req.ProfileToken = profiles[self._profile_index]._token - uri_no_auth = self._media_service.GetStreamUri(req).Uri + _LOGGER.debug("Retrieving stream uri") + + req = media_service.create_type('GetStreamUri') + req.ProfileToken = profiles[self._profile_index].token + req.StreamSetup = {'Stream': 'RTP-Unicast', + 'Transport': {'Protocol': 'RTSP'}} + + stream_uri = await media_service.GetStreamUri(req) + uri_no_auth = stream_uri.Uri uri_for_log = uri_no_auth.replace( 'rtsp://', 'rtsp://:@', 1) self._input = uri_no_auth.replace( 'rtsp://', 'rtsp://{}:{}@'.format(self._username, self._password), 1) + _LOGGER.debug( "ONVIF Camera Using the following URL for %s: %s", self._name, uri_for_log) - # we won't need the media service anymore - self._media_service = None except exceptions.ONVIFError as err: - _LOGGER.debug("Couldn't setup camera '%s'. Error: %s", + _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) return - def perform_ptz(self, pan, tilt, zoom): + async def async_perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" from onvif import exceptions + + if self._ptz_service is None: + _LOGGER.warning("PTZ actions are not supported on camera '%s'", + self._name) + return + if self._ptz_service: pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0 tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0 @@ -164,7 +274,11 @@ class ONVIFHassCamera(Camera): "PanTilt": {"_x": pan_val, "_y": tilt_val}, "Zoom": {"_x": zoom_val}}} try: - self._ptz_service.ContinuousMove(req) + _LOGGER.debug( + "Calling PTZ | Pan = %d | Tilt = %d | Zoom = %d", + pan_val, tilt_val, zoom_val) + + await self._ptz_service.ContinuousMove(req) except exceptions.ONVIFError as err: if "Bad Request" in err.reason: self._ptz_service = None @@ -175,20 +289,18 @@ class ONVIFHassCamera(Camera): async def async_added_to_hass(self): """Handle entity addition to hass.""" + _LOGGER.debug("Camera '%s' added to hass", self._name) + if ONVIF_DATA not in self.hass.data: self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES].append(self) - await self.hass.async_add_executor_job(self.obtain_input_uri) async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg.tools import ImageFrame, IMAGE_JPEG - if not self._input: - await self.hass.async_add_executor_job(self.obtain_input_uri) - if not self._input: - return None + _LOGGER.debug("Retrieving image from camera '%s'", self._name) ffmpeg = ImageFrame( self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) @@ -202,14 +314,12 @@ class ONVIFHassCamera(Camera): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg.camera import CameraMjpeg - if not self._input: - await self.hass.async_add_executor_job(self.obtain_input_uri) - if not self._input: - return None + _LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop) + await stream.open_camera( self._input, extra_cmd=self._ffmpeg_arguments) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index bade9f37022..177a8e1eabd 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -3,12 +3,10 @@ "name": "Onvif", "documentation": "https://www.home-assistant.io/components/onvif", "requirements": [ - "onvif-py3==0.1.3", - "suds-passworddigest-homeassistant==0.1.2a0.dev0", - "suds-py3==1.3.3.0" + "onvif-zeep-async==0.1.2" ], "dependencies": [ "ffmpeg" ], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index c68a6a17192..889ceed3690 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -788,7 +788,7 @@ oemthermostat==1.1 onkyo-eiscp==1.2.4 # homeassistant.components.onvif -onvif-py3==0.1.3 +onvif-zeep-async==0.1.2 # homeassistant.components.openevse openevsewifi==0.4 @@ -1670,12 +1670,6 @@ stringcase==1.2.0 # homeassistant.components.ecovacs sucks==0.9.4 -# homeassistant.components.onvif -suds-passworddigest-homeassistant==0.1.2a0.dev0 - -# homeassistant.components.onvif -suds-py3==1.3.3.0 - # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3