Esempio n. 1
0
class ONVIFCLI(Cmd):
    prompt = 'ONVIF >>> '
    client = None
    cmd_parser = None

    def setup(self, args):
        ''' `args`: Instance of `argparse.ArgumentParser` '''
        # Create onvif camera client
        self.client = ONVIFCamera(args.host,
                                  args.port,
                                  args.user,
                                  args.password,
                                  args.wsdl,
                                  encrypt=args.encrypt)

        # Create cmd argument parser
        self.create_cmd_parser()

    def create_cmd_parser(self):
        # Create parser to parse CMD, `params` is optional.
        cmd_parser = ThrowingArgumentParser(
            prog='ONVIF CMD', usage='CMD service operation [params]')
        cmd_parser.add_argument('service')
        cmd_parser.add_argument('operation')
        cmd_parser.add_argument('params', default='{}', nargs=REMAINDER)
        self.cmd_parser = cmd_parser

    def do_cmd(self, line):
        '''Usage: CMD service operation [parameters]'''
        try:
            args = self.cmd_parser.parse_args(line.split())
        except ValueError as err:
            return error(err)

        # Check if args.service is valid
        if args.service not in SUPPORTED_SERVICES:
            return error('No Service: ' + args.service)

        args.params = ''.join(args.params)
        # params is optional
        if not args.params.strip():
            args.params = '{}'

        # params must be a dictionary format string
        match = re.match(r"^.*?(\{.*\}).*$", args.params)
        if not match:
            return error('Invalid params')

        try:
            args.params = dict(literal_eval(match.group(1)))
        except ValueError as err:
            return error('Invalid params')

        try:
            # Get ONVIF service
            service = self.client.get_service(args.service)
            # Actually execute the command and get the response
            response = getattr(service, args.operation)(args.params)
        except MethodNotFound as err:
            return error('No Operation: %s' % args.operation)
        except Exception as err:
            return error(err)

        if isinstance(response, (Text, bool)):
            return success(response)
        # Try to convert instance to dictionary
        try:
            success(ONVIFService.to_dict(response))
        except ONVIFError:
            error({})

    def complete_cmd(self, text, line, begidx, endidx):
        # TODO: complete service operations
        # service.ws_client.service._ServiceSelector__services[0].ports[0].methods.keys()
        if not text:
            completions = SUPPORTED_SERVICES[:]
        else:
            completions = [
                key for key in SUPPORTED_SERVICES if key.startswith(text)
            ]
        return completions

    def emptyline(self):
        return ''

    def do_EOF(self, line):
        return True
Esempio n. 2
0
class ONVIFHassCamera(Camera):
    """An implementation of an ONVIF camera."""
    def __init__(self, hass, config):
        """Initialize an ONVIF camera."""
        super().__init__()

        _LOGGER.debug("Importing dependencies")

        _LOGGER.debug("Setting up the ONVIF camera component")

        self._username = config.get(CONF_USERNAME)
        self._password = config.get(CONF_PASSWORD)
        self._host = config.get(CONF_HOST)
        self._port = config.get(CONF_PORT)
        self._name = config.get(CONF_NAME)
        self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
        self._profile_index = config.get(CONF_PROFILE_IDX)
        self._rtsp_transport = config.get(CONF_RTSP_TRANSPORT)
        self._media_service = None
        self._ptz_service = None
        self._image_service = None
        self._input_uri = None
        self._input_uri_for_log = None
        self._profile_token = None
        self._profiles = None
        self._ptz_opt = None
        self._ptz_presets = None

        _LOGGER.debug("Setting up the ONVIF camera device @ '%s:%s'",
                      self._host, self._port)

        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.
        """
        try:
            _LOGGER.debug("Updating service addresses")
            await self._camera.update_xaddrs()
            await self.async_check_date_and_time()

            self._media_service = await self.async_obtain_media_service()
            self._profiles = await self.async_obtain_profiles()
            self._profile_token = self.index_to_profile_token()
            await self.async_obtain_input_uri()
            self._ptz_service = await self.async_obtain_ptz_service()

        except ClientConnectionError 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,
            )

    async def async_check_date_and_time(self):
        """Warns if camera and system date not synced."""
        _LOGGER.debug("Setting up the ONVIF device management service")
        devicemgmt = self._camera.create_devicemgmt_service()

        _LOGGER.debug("Retrieving current camera date/time")
        try:
            system_date = dt_util.utcnow()
            device_time = await devicemgmt.GetSystemDateAndTime()
            if not device_time:
                _LOGGER.debug(
                    """Couldn't get camera '%s' date/time.
                    GetSystemDateAndTime() return null/empty""",
                    self._name,
                )
                return

            if device_time.UTCDateTime:
                tzone = dt_util.UTC
                cdate = device_time.UTCDateTime
            else:
                tzone = (dt_util.get_time_zone(device_time.TimeZone)
                         or dt_util.DEFAULT_TIME_ZONE)
                cdate = device_time.LocalDateTime

            if cdate is None:
                _LOGGER.warning("Could not retrieve date/time on this camera")
            else:
                cam_date = dt.datetime(
                    cdate.Date.Year,
                    cdate.Date.Month,
                    cdate.Date.Day,
                    cdate.Time.Hour,
                    cdate.Time.Minute,
                    cdate.Time.Second,
                    0,
                    tzone,
                )

                cam_date_utc = cam_date.astimezone(dt_util.UTC)

                _LOGGER.debug("TimeZone for date/time: %s", tzone)

                _LOGGER.debug("Camera date/time: %s", cam_date)

                _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc)

                _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 (UTC) is '%s', "
                        "which is different from the system '%s', "
                        "this could lead to authentication issues",
                        cam_date_utc,
                        system_date,
                    )
        except ServerDisconnectedError as err:
            _LOGGER.warning("Couldn't get camera '%s' date/time. Error: %s",
                            self._name, err)

    async def async_obtain_profiles(self):
        """Obtain onvif profiles object."""
        try:
            __profiles = await self._media_service.GetProfiles()
            _LOGGER.debug("Retrieved '%d' profiles", len(__profiles))
            return __profiles
        except exceptions.ONVIFError as err:
            _LOGGER.error(
                "Couldn't retrieve profiles of camera '%s'. Error: %s",
                self._name,
                err,
            )
            return None

    async def async_obtain_media_service(self):
        """Obtain onvif profiles object."""
        try:
            _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
                          self._host, self._port)
            __media_service = self._camera.create_media_service()
            return __media_service
        except exceptions.ONVIFError as err:
            _LOGGER.error(
                "Couldn't retrieve media_service of camera '%s'. Error: %s",
                self._name,
                err,
            )
            return None

    def index_to_profile_token(self):
        """Return token name from a index over profiles object."""
        if self._profile_index >= len(self._profiles):
            _LOGGER.warning(
                "ONVIF Camera '%s' doesn't provide profile %d."
                " Using the last profile.",
                self._name,
                self._profile_index,
            )
            self._profile_index = -1

        _LOGGER.debug("Using profile index '%d'", self._profile_index)
        return self._profiles[self._profile_index].token

    async def async_obtain_input_uri(self):
        """Set the input uri for the camera."""
        _LOGGER.debug("Retrieving stream uri")
        # Fix Onvif setup error on Goke GK7102 based IP camera #26781
        # Assume not buggy camera and see if ClientConnectionError
        # reload fresh self._media_service before one retry
        for i in range(0, 2):
            try:
                req = self._media_service.create_type("GetStreamUri")
                req.ProfileToken = self._profile_token
                req.StreamSetup = {
                    "Stream": "RTP-Unicast",
                    "Transport": {
                        "Protocol": self._rtsp_transport
                    },
                }

                stream_uri = await self._media_service.GetStreamUri(req)
                uri_no_auth = stream_uri.Uri
                self._input_uri_for_log = uri_no_auth.replace(
                    "rtsp://", "rtsp://<user>:<password>@", 1)

                self._input_uri = uri_no_auth.replace(
                    "rtsp://",
                    "rtsp://%s:%s@" % (self._username, self._password), 1)

                _LOGGER.debug(
                    "ONVIF Camera Using the following URL for %s: %s",
                    self._name,
                    self._input_uri_for_log,
                )
                break
            except ClientConnectionError as err:
                if i == 0:
                    _LOGGER.info(
                        "GetStreamUri on '%s'. Error: %s. Trying a workaround against known issue(#26781)",
                        self._name,
                        err,
                    )
                    self._media_service = self._camera.create_media_service()
                    pass
                else:
                    _LOGGER.error("Couldn't setup camera '%s'. Error: %s",
                                  self._name, err)
                    return (None, None)

    async def async_obtain_ptz_service(self):
        """Set up PTZ service if available."""
        _LOGGER.debug("Setting up the ONVIF PTZ service")
        if self._camera.get_service("ptz") is None:
            _LOGGER.debug("PTZ is not available")
            return None
        else:
            _LOGGER.debug("Completed set up of the ONVIF camera component")
            return self._camera.create_ptz_service()

    async def async_perform_ptz_move(self, pan, tilt, zoom, distance, speed,
                                     move_mode, continuous_timeout,
                                     timeout_compliance):
        """Perform legacy PTZ actions on the camera + new move_modes"""
        pan_val = (distance if pan == DIR_RIGHT else
                   -distance if pan == DIR_LEFT else 0)
        tilt_val = (distance if tilt == DIR_UP else
                    -distance if tilt == DIR_DOWN else 0)
        zoom_val = (distance if zoom == ZOOM_IN else
                    -distance if zoom == ZOOM_OUT else 0)
        speed_val = speed
        await self.async_perform_ptz_advanced_move(
            (pan_val, tilt_val, zoom_val), (speed_val, speed_val, speed_val),
            move_mode, continuous_timeout, timeout_compliance)

    async def async_perform_ptz_advanced_move(self, ptz_vector, speed_vector,
                                              move_mode, continuous_timeout,
                                              timeout_compliance):
        """Perform a PTZ action on the camera."""
        if self._ptz_service is None:
            _LOGGER.warning(
                "PTZ Move actions are not supported on camera '%s'",
                self._name)
            return

        if self._ptz_service:
            pan_val = ptz_vector[0]
            tilt_val = ptz_vector[1]
            zoom_val = ptz_vector[2]
            _LOGGER.debug(
                "Calling %s PTZ Move on camera '%s'| Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %s | Timeout = %1.1f",
                move_mode, self._name, ptz_vector[0], ptz_vector[1],
                ptz_vector[2], speed_vector, continuous_timeout)
            try:
                req = self._ptz_service.create_type(move_mode)
                req.ProfileToken = self._profile_token

                if move_mode == CONTINUOUS_MOVE:
                    req.Velocity = {
                        "PanTilt": {
                            "x": pan_val,
                            "y": tilt_val
                        },
                        "Zoom": {
                            "x": zoom_val
                        },
                    }
                    if continuous_timeout != 0:
                        req.Timeout = dt.timedelta(
                            0, 0, continuous_timeout * 1000000)
                    await self._ptz_service.ContinuousMove(req)
                    if continuous_timeout != 0 and not timeout_compliance:
                        await asyncio.sleep(continuous_timeout)
                        req = self._ptz_service.create_type("Stop")
                        req.ProfileToken = self._profile_token
                        await self._ptz_service.Stop(req)

                elif move_mode == RELATIVE_MOVE:
                    req.Translation = {
                        "PanTilt": {
                            "x": pan_val,
                            "y": tilt_val
                        },
                        "Zoom": {
                            "x": zoom_val
                        },
                    }
                    req.Speed = {
                        "PanTilt": {
                            "x": speed_vector[0],
                            "y": speed_vector[1]
                        },
                        "Zoom": {
                            "x": speed_vector[2]
                        },
                    }
                    await self._ptz_service.RelativeMove(req)

                elif move_mode == ABSOLUTE_MOVE:
                    req.Position = {
                        "PanTilt": {
                            "x": pan_val,
                            "y": tilt_val
                        },
                        "Zoom": {
                            "x": zoom_val
                        },
                    }
                    req.Speed = {
                        "PanTilt": {
                            "x": speed_vector[0],
                            "y": speed_vector[1]
                        },
                        "Zoom": {
                            "x": speed_vector[2]
                        },
                    }
                    await self._ptz_service.AbsoluteMove(req)

            except exceptions.ONVIFError as err:
                if "Bad Request" in err.reason:
                    self._ptz_service = None
                    _LOGGER.debug("Camera '%s' doesn't support PTZ.",
                                  self._name)
        else:
            _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

    async def async_perform_ptz_preset(self, preset_operation, preset_name,
                                       preset_token):
        """Perform a PTZ Preset action on the camera."""
        if self._ptz_service:
            if preset_operation in (
                    GOTO_HOME,
                    SET_HOME,
                    GOTO_PRESET,
                    SET_PRESET,
                    GET_PRESETS,
            ):
                try:
                    _LOGGER.debug(
                        "Calling PTZ preset| Operation = %s | PresetName = %s | PresetToken = %s",
                        preset_operation,
                        preset_name,
                        preset_token,
                    )

                    req = self._ptz_service.create_type(preset_operation)
                    req.ProfileToken = self._profile_token

                    if preset_operation == GOTO_PRESET:
                        preset_token = next(
                            (preset["token"] for preset in self._ptz_presets
                             if preset["Name"] == preset_name),
                            None,
                        )
                        print(self._ptz_presets)
                        _LOGGER.debug(
                            "PresetToken from PresetName | PresetName = %s | PresetToken = %s",
                            preset_name,
                            preset_token,
                        )
                        print(
                            "PresetToken from PresetName | PresetName = %s | PresetToken = %s"
                            % (preset_name, preset_token))
                        req.PresetToken = "%s" % preset_token
                        req.Speed = {
                            "PanTilt": {
                                "x": 1.0,
                                "y": 1.0
                            },
                            "Zoom": {
                                "x": 1.0
                            },
                        }
                        await self._ptz_service.GotoPreset(req)

                    if preset_operation == SET_PRESET:
                        req.PresetToken = preset_token
                        req.PresetName = preset_name
                        await self._ptz_service.SetPreset(req)

                    if preset_operation == GET_PRESETS:
                        __presets = await self._ptz_service.GetPresets(req)
                        presets = []
                        if __presets is not None:
                            for preset in __presets:
                                presets.append(preset["Name"])
                        pn.create(self.hass,
                                  "\n".join(presets),
                                  title="Onvif PTZ Presets")

                    if preset_operation == GOTO_HOME:
                        await self._ptz_service.GotoHomePosition(req)

                    if preset_operation == SET_HOME:
                        await self._ptz_service.SetHomePosition(req)

                except exceptions.ONVIFError as err:
                    if "Bad Request" in err.reason:
                        _LOGGER.error(
                            "Camera '%s' doesn't support PTZ %s operation.",
                            self._name,
                            preset_operation,
                        )
                except Exception as err:
                    _LOGGER.info(
                        "Camera '%s' PTZ %s operation failed with that reason: %s",
                        self._name,
                        preset_operation,
                        err,
                    )
            else:
                _LOGGER.debug("PTZ %s operation is not implemented",
                              preset_operation)
        else:
            self._ptz_service = None
            _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

    async def async_perform_reboot(self):
        """Perform a SystemReboot action on the camera."""
        try:
            _LOGGER.debug("Calling SystemReboot")
            ret = await self._camera.devicemgmt.SystemReboot()
            _LOGGER.debug("Camera '%s' Reboot command returned '%s'",
                          self._name, ret)
        except exceptions.ONVIFError as err:
            _LOGGER.error(
                "Couldn't reboot the camera '%s', please verify "
                "that the camera supports the command. Error: %s",
                self._name,
                err,
            )

    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)

    async def async_camera_image(self):
        """Return a still image response from the camera."""

        _LOGGER.debug("Retrieving image from camera '%s'", self._name)

        ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary,
                            loop=self.hass.loop)

        image = await asyncio.shield(
            ffmpeg.get_image(
                self._input_uri,
                output_format=IMAGE_JPEG,
                extra_cmd=self._ffmpeg_arguments,
            ))
        return image

    async def handle_async_mjpeg_stream(self, request):
        """Generate an HTTP MJPEG stream from the camera."""
        _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_uri,
                                 extra_cmd=self._ffmpeg_arguments)

        try:
            stream_reader = await stream.get_reader()
            return await async_aiohttp_proxy_stream(
                self.hass,
                request,
                stream_reader,
                ffmpeg_manager.ffmpeg_stream_content_type,
            )
        finally:
            await stream.close()

    @property
    def supported_features(self):
        """Return supported features."""
        if self._input_uri:
            return SUPPORT_STREAM
        return 0

    async def stream_source(self):
        """Return the stream source."""
        return self._input_uri

    @property
    def name(self):
        """Return the name of this camera."""
        return self._name
Esempio n. 3
0
class ONVIFHassCamera(Camera):
    """An implementation of an ONVIF camera."""

    def __init__(self, hass, config):
        """Initialize an ONVIF camera."""
        super().__init__()

        _LOGGER.debug("Importing dependencies")

        _LOGGER.debug("Setting up the ONVIF camera component")

        self._username = config.get(CONF_USERNAME)
        self._password = config.get(CONF_PASSWORD)
        self._host = config.get(CONF_HOST)
        self._port = config.get(CONF_PORT)
        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

        _LOGGER.debug(
            "Setting up the ONVIF camera device @ '%s:%s'", self._host, self._port
        )

        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.
        """
        try:
            _LOGGER.debug("Updating service addresses")
            await self._camera.update_xaddrs()

            await self.async_check_date_and_time()
            await self.async_obtain_input_uri()
            await self.setup_ptz()
        except ClientConnectionError 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,
            )

    async def async_check_date_and_time(self):
        """Warns if camera and system date not synced."""
        _LOGGER.debug("Setting up the ONVIF device management service")
        devicemgmt = self._camera.create_devicemgmt_service()

        _LOGGER.debug("Retrieving current camera date/time")
        try:
            system_date = dt_util.utcnow()
            device_time = await devicemgmt.GetSystemDateAndTime()
            if not device_time:
                _LOGGER.debug(
                    """Couldn't get camera '%s' date/time.
                    GetSystemDateAndTime() return null/empty""",
                    self._name,
                )
                return

            if device_time.UTCDateTime:
                tzone = dt_util.UTC
                cdate = device_time.UTCDateTime
            else:
                tzone = (
                    dt_util.get_time_zone(device_time.TimeZone)
                    or dt_util.DEFAULT_TIME_ZONE
                )
                cdate = device_time.LocalDateTime

            if cdate is None:
                _LOGGER.warning("Could not retrieve date/time on this camera")
            else:
                cam_date = dt.datetime(
                    cdate.Date.Year,
                    cdate.Date.Month,
                    cdate.Date.Day,
                    cdate.Time.Hour,
                    cdate.Time.Minute,
                    cdate.Time.Second,
                    0,
                    tzone,
                )

                cam_date_utc = cam_date.astimezone(dt_util.UTC)

                _LOGGER.debug("TimeZone for date/time: %s", tzone)

                _LOGGER.debug("Camera date/time: %s", cam_date)

                _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc)

                _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 (UTC) is '%s', "
                        "which is different from the system '%s', "
                        "this could lead to authentication issues",
                        cam_date_utc,
                        system_date,
                    )
        except ServerDisconnectedError as err:
            _LOGGER.warning(
                "Couldn't get camera '%s' date/time. Error: %s", self._name, err
            )

    async def async_obtain_input_uri(self):
        """Set the input uri for the camera."""
        _LOGGER.debug(
            "Connecting with ONVIF Camera: %s on port %s", self._host, self._port
        )

        try:
            _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."
                    " Using the last profile.",
                    self._name,
                    self._profile_index,
                )
                self._profile_index = -1

            _LOGGER.debug("Using profile index '%d'", self._profile_index)

            _LOGGER.debug("Retrieving stream uri")

            # Fix Onvif setup error on Goke GK7102 based IP camera
            # where we need to recreate media_service  #26781
            media_service = self._camera.create_media_service()

            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://<user>:<password>@", 1)
            self._input = uri_no_auth.replace(
                "rtsp://", f"rtsp://{self._username}:{self._password}@", 1
            )

            _LOGGER.debug(
                "ONVIF Camera Using the following URL for %s: %s",
                self._name,
                uri_for_log,
            )
        except exceptions.ONVIFError as err:
            _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err)

    async def setup_ptz(self):
        """Set up PTZ if available."""
        _LOGGER.debug("Setting up the ONVIF PTZ service")
        if self._camera.get_service("ptz") is None:
            _LOGGER.debug("PTZ is not available")
        else:
            self._ptz_service = self._camera.create_ptz_service()
        _LOGGER.debug("Completed set up of the ONVIF camera component")

    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
        media_service = self._camera.create_media_service()

        profiles = await media_service.GetProfiles()
        
        req = self._ptz_service.create_type("ContinuousMove")
        req.ProfileToken = profiles[self._profile_index].token
        
        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
            zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
            req.Velocity = {
                "PanTilt": {"x": pan_val, "y": tilt_val},
                "Zoom": {"x": zoom_val},
            }
            try:
                _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
                    _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
        else:
            _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

    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)

    async def async_camera_image(self):
        """Return a still image response from the camera."""

        _LOGGER.debug("Retrieving image from camera '%s'", self._name)

        ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)

        image = await asyncio.shield(
            ffmpeg.get_image(
                self._input, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments
            )
        )
        return image

    async def handle_async_mjpeg_stream(self, request):
        """Generate an HTTP MJPEG stream from the camera."""
        _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)

        try:
            stream_reader = await stream.get_reader()
            return await async_aiohttp_proxy_stream(
                self.hass,
                request,
                stream_reader,
                ffmpeg_manager.ffmpeg_stream_content_type,
            )
        finally:
            await stream.close()

    @property
    def supported_features(self):
        """Return supported features."""
        if self._input:
            return SUPPORT_STREAM
        return 0

    async def stream_source(self):
        """Return the stream source."""
        return self._input

    @property
    def name(self):
        """Return the name of this camera."""
        return self._name
Esempio n. 4
0
class ONVIFHassCamera(Camera):
    """An implementation of an ONVIF camera."""
    def __init__(self, hass, config):
        """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)
        self._host = config.get(CONF_HOST)
        self._port = config.get(CONF_PORT)
        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

        _LOGGER.debug("Setting up the ONVIF camera device @ '%s:%s'",
                      self._host, self._port)

        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:
            _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."
                    " Using the last profile.", self._name,
                    self._profile_index)
                self._profile_index = -1

            _LOGGER.debug("Using profile index '%d'", self._profile_index)

            _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://<user>:<password>@', 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)
        except exceptions.ONVIFError as err:
            _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name,
                          err)
            return

    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
            zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
            req = {
                "Velocity": {
                    "PanTilt": {
                        "_x": pan_val,
                        "_y": tilt_val
                    },
                    "Zoom": {
                        "_x": zoom_val
                    }
                }
            }
            try:
                _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
                    _LOGGER.debug("Camera '%s' doesn't support PTZ.",
                                  self._name)
        else:
            _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

    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)

    async def async_camera_image(self):
        """Return a still image response from the camera."""
        from haffmpeg.tools import ImageFrame, IMAGE_JPEG

        _LOGGER.debug("Retrieving image from camera '%s'", self._name)

        ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary,
                            loop=self.hass.loop)

        image = await asyncio.shield(ffmpeg.get_image(
            self._input,
            output_format=IMAGE_JPEG,
            extra_cmd=self._ffmpeg_arguments),
                                     loop=self.hass.loop)
        return image

    async def handle_async_mjpeg_stream(self, request):
        """Generate an HTTP MJPEG stream from the camera."""
        from haffmpeg.camera import CameraMjpeg

        _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)

        try:
            stream_reader = await stream.get_reader()
            return await async_aiohttp_proxy_stream(
                self.hass, request, stream_reader,
                ffmpeg_manager.ffmpeg_stream_content_type)
        finally:
            await stream.close()

    @property
    def supported_features(self):
        """Return supported features."""
        if self._input:
            return SUPPORT_STREAM
        return 0

    @property
    def stream_source(self):
        """Return the stream source."""
        return self._input

    @property
    def name(self):
        """Return the name of this camera."""
        return self._name
Esempio n. 5
0
class ONVIFHassCamera(Camera):
    """An implementation of an ONVIF camera."""
    def __init__(self, hass, config):
        """Initialize an ONVIF camera."""
        super().__init__()

        _LOGGER.debug("Importing dependencies")

        _LOGGER.debug("Setting up the ONVIF camera component")

        self._username = config.get(CONF_USERNAME)
        self._password = config.get(CONF_PASSWORD)
        self._host = config.get(CONF_HOST)
        self._port = config.get(CONF_PORT)
        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._snapshot = None
        self.stream_options[CONF_RTSP_TRANSPORT] = config.get(
            CONF_RTSP_TRANSPORT)
        self._mac = None

        _LOGGER.debug("Setting up the ONVIF camera device @ '%s:%s'",
                      self._host, self._port)

        session = async_get_clientsession(hass)
        transport = AsyncTransport(None, session=session)
        self._camera = ONVIFCamera(
            self._host,
            self._port,
            self._username,
            self._password,
            "{}/wsdl/".format(os.path.dirname(onvif.__file__)),
            transport=transport,
        )

    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.
        """
        try:
            _LOGGER.debug("Updating service addresses")
            await self._camera.update_xaddrs()

            await self.async_obtain_mac_address()
            await self.async_check_date_and_time()
            await self.async_obtain_input_uri()
            await self.async_obtain_snapshot_uri()
            self.setup_ptz()
        except ClientConnectionError 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,
            )

    async def async_obtain_mac_address(self):
        """Obtain the MAC address of the camera to use as the unique ID."""
        devicemgmt = self._camera.create_devicemgmt_service()
        network_interfaces = await devicemgmt.GetNetworkInterfaces()
        for interface in network_interfaces:
            if interface.Enabled:
                self._mac = interface.Info.HwAddress

    async def async_check_date_and_time(self):
        """Warns if camera and system date not synced."""
        _LOGGER.debug("Setting up the ONVIF device management service")
        devicemgmt = self._camera.create_devicemgmt_service()

        _LOGGER.debug("Retrieving current camera date/time")
        try:
            system_date = dt_util.utcnow()
            device_time = await devicemgmt.GetSystemDateAndTime()
            if not device_time:
                _LOGGER.debug(
                    """Couldn't get camera '%s' date/time.
                    GetSystemDateAndTime() return null/empty""",
                    self._name,
                )
                return

            if device_time.UTCDateTime:
                tzone = dt_util.UTC
                cdate = device_time.UTCDateTime
            else:
                tzone = (dt_util.get_time_zone(device_time.TimeZone)
                         or dt_util.DEFAULT_TIME_ZONE)
                cdate = device_time.LocalDateTime

            if cdate is None:
                _LOGGER.warning("Could not retrieve date/time on this camera")
            else:
                cam_date = dt.datetime(
                    cdate.Date.Year,
                    cdate.Date.Month,
                    cdate.Date.Day,
                    cdate.Time.Hour,
                    cdate.Time.Minute,
                    cdate.Time.Second,
                    0,
                    tzone,
                )

                cam_date_utc = cam_date.astimezone(dt_util.UTC)

                _LOGGER.debug("TimeZone for date/time: %s", tzone)

                _LOGGER.debug("Camera date/time: %s", cam_date)

                _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc)

                _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 (UTC) is '%s', "
                        "which is different from the system '%s', "
                        "this could lead to authentication issues",
                        cam_date_utc,
                        system_date,
                    )
        except ServerDisconnectedError as err:
            _LOGGER.warning("Couldn't get camera '%s' date/time. Error: %s",
                            self._name, err)

    async def async_obtain_profile_token(self):
        """Obtain profile token to use with requests."""
        try:
            media_service = self._camera.get_service("media")

            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."
                    " Using the last profile.",
                    self._name,
                    self._profile_index,
                )
                self._profile_index = -1

            _LOGGER.debug("Using profile index '%d'", self._profile_index)

            return profiles[self._profile_index].token
        except exceptions.ONVIFError as err:
            _LOGGER.error(
                "Couldn't retrieve profile token of camera '%s'. Error: %s",
                self._name,
                err,
            )
            return None

    async def async_obtain_input_uri(self):
        """Set the input uri for the camera."""
        _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
                      self._host, self._port)

        try:
            _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."
                    " Using the last profile.",
                    self._name,
                    self._profile_index,
                )
                self._profile_index = -1

            _LOGGER.debug("Using profile index '%d'", self._profile_index)

            _LOGGER.debug("Retrieving stream uri")

            # Fix Onvif setup error on Goke GK7102 based IP camera
            # where we need to recreate media_service  #26781
            media_service = self._camera.create_media_service()

            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://<user>:<password>@", 1)
            self._input = uri_no_auth.replace(
                "rtsp://", f"rtsp://{self._username}:{self._password}@", 1)

            _LOGGER.debug(
                "ONVIF Camera Using the following URL for %s: %s",
                self._name,
                uri_for_log,
            )
        except exceptions.ONVIFError as err:
            _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name,
                          err)

    async def async_obtain_snapshot_uri(self):
        """Set the snapshot uri for the camera."""
        _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
                      self._host, self._port)

        try:
            _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."
                    " Using the last profile.",
                    self._name,
                    self._profile_index,
                )
                self._profile_index = -1

            _LOGGER.debug("Using profile index '%d'", self._profile_index)

            _LOGGER.debug("Retrieving snapshot uri")

            # Fix Onvif setup error on Goke GK7102 based IP camera
            # where we need to recreate media_service  #26781
            media_service = self._camera.create_media_service()

            req = media_service.create_type("GetSnapshotUri")
            req.ProfileToken = profiles[self._profile_index].token

            try:
                snapshot_uri = await media_service.GetSnapshotUri(req)
                self._snapshot = snapshot_uri.Uri
            except ServerDisconnectedError as err:
                _LOGGER.debug("Camera does not support GetSnapshotUri: %s",
                              err)

            _LOGGER.debug(
                "ONVIF Camera Using the following URL for %s snapshot: %s",
                self._name,
                self._snapshot,
            )
        except exceptions.ONVIFError as err:
            _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name,
                          err)

    def setup_ptz(self):
        """Set up PTZ if available."""
        _LOGGER.debug("Setting up the ONVIF PTZ service")
        if self._camera.get_service("ptz", create=False) is None:
            _LOGGER.debug("PTZ is not available")
        else:
            self._ptz_service = self._camera.create_ptz_service()
        _LOGGER.debug("Completed set up of the ONVIF camera component")

    async def async_perform_ptz(self, pan, tilt, zoom, distance, speed,
                                move_mode, continuous_duration):
        """Perform a PTZ action on the camera."""
        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 = distance * PAN_FACTOR.get(pan, 0)
            tilt_val = distance * TILT_FACTOR.get(tilt, 0)
            zoom_val = distance * ZOOM_FACTOR.get(zoom, 0)
            speed_val = speed
            _LOGGER.debug(
                "Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f",
                move_mode,
                pan_val,
                tilt_val,
                zoom_val,
                speed_val,
            )
            try:
                req = self._ptz_service.create_type(move_mode)
                req.ProfileToken = await self.async_obtain_profile_token()
                if move_mode == CONTINUOUS_MOVE:
                    req.Velocity = {
                        "PanTilt": {
                            "x": pan_val,
                            "y": tilt_val
                        },
                        "Zoom": {
                            "x": zoom_val
                        },
                    }

                    await self._ptz_service.ContinuousMove(req)
                    await asyncio.sleep(continuous_duration)
                    req = self._ptz_service.create_type("Stop")
                    req.ProfileToken = await self.async_obtain_profile_token()
                    await self._ptz_service.Stop(
                        {"ProfileToken": req.ProfileToken})
                elif move_mode == RELATIVE_MOVE:
                    req.Translation = {
                        "PanTilt": {
                            "x": pan_val,
                            "y": tilt_val
                        },
                        "Zoom": {
                            "x": zoom_val
                        },
                    }
                    req.Speed = {
                        "PanTilt": {
                            "x": speed_val,
                            "y": speed_val
                        },
                        "Zoom": {
                            "x": speed_val
                        },
                    }
                    await self._ptz_service.RelativeMove(req)
                elif move_mode == ABSOLUTE_MOVE:
                    req.Position = {
                        "PanTilt": {
                            "x": pan_val,
                            "y": tilt_val
                        },
                        "Zoom": {
                            "x": zoom_val
                        },
                    }
                    req.Speed = {
                        "PanTilt": {
                            "x": speed_val,
                            "y": speed_val
                        },
                        "Zoom": {
                            "x": speed_val
                        },
                    }
                    await self._ptz_service.AbsoluteMove(req)
            except exceptions.ONVIFError as err:
                if "Bad Request" in err.reason:
                    self._ptz_service = None
                    _LOGGER.debug("Camera '%s' doesn't support PTZ.",
                                  self._name)
        else:
            _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

    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)

    async def async_camera_image(self):
        """Return a still image response from the camera."""
        _LOGGER.debug("Retrieving image from camera '%s'", self._name)
        image = None

        if self._snapshot is not None:
            auth = None
            if self._username and self._password:
                auth = HTTPDigestAuth(self._username, self._password)

            def fetch():
                """Read image from a URL."""
                try:
                    response = requests.get(self._snapshot,
                                            timeout=5,
                                            auth=auth)
                    if response.status_code < 300:
                        return response.content
                except requests.exceptions.RequestException as error:
                    _LOGGER.error(
                        "Fetch snapshot image failed from %s, falling back to FFmpeg; %s",
                        self._name,
                        error,
                    )

                return None

            image = await self.hass.async_add_job(fetch)

        if image is None:
            # Don't keep trying the snapshot URL
            self._snapshot = None

            ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary,
                                loop=self.hass.loop)
            image = await asyncio.shield(
                ffmpeg.get_image(
                    self._input,
                    output_format=IMAGE_JPEG,
                    extra_cmd=self._ffmpeg_arguments,
                ))

        return image

    async def handle_async_mjpeg_stream(self, request):
        """Generate an HTTP MJPEG stream from the camera."""
        _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)

        try:
            stream_reader = await stream.get_reader()
            return await async_aiohttp_proxy_stream(
                self.hass,
                request,
                stream_reader,
                ffmpeg_manager.ffmpeg_stream_content_type,
            )
        finally:
            await stream.close()

    @property
    def supported_features(self):
        """Return supported features."""
        if self._input:
            return SUPPORT_STREAM
        return 0

    async def stream_source(self):
        """Return the stream source."""
        return self._input

    @property
    def name(self):
        """Return the name of this camera."""
        return self._name

    @property
    def unique_id(self) -> Optional[str]:
        """Return a unique ID."""
        if self._profile_index:
            return f"{self._mac}_{self._profile_index}"
        return self._mac
Esempio n. 6
0
class ONVIFHassCamera(Camera):
    """An implementation of an ONVIF camera."""

    def __init__(self, hass, config):
        """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)
        self._host = config.get(CONF_HOST)
        self._port = config.get(CONF_PORT)
        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

        _LOGGER.debug("Setting up the ONVIF camera device @ '%s:%s'",
                      self._host,
                      self._port)

        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()
            if device_time:
                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:
            _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."
                                " Using the last profile.",
                                self._name, self._profile_index)
                self._profile_index = -1

            _LOGGER.debug("Using profile index '%d'",
                          self._profile_index)

            _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://<user>:<password>@', 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)
        except exceptions.ONVIFError as err:
            _LOGGER.error("Couldn't setup camera '%s'. Error: %s",
                          self._name, err)
            return

    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
            zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
            req = {"Velocity": {
                "PanTilt": {"_x": pan_val, "_y": tilt_val},
                "Zoom": {"_x": zoom_val}}}
            try:
                _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
                    _LOGGER.debug("Camera '%s' doesn't support PTZ.",
                                  self._name)
        else:
            _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)

    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)

    async def async_camera_image(self):
        """Return a still image response from the camera."""
        from haffmpeg.tools import ImageFrame, IMAGE_JPEG

        _LOGGER.debug("Retrieving image from camera '%s'", self._name)

        ffmpeg = ImageFrame(
            self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)

        image = await asyncio.shield(ffmpeg.get_image(
            self._input, output_format=IMAGE_JPEG,
            extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
        return image

    async def handle_async_mjpeg_stream(self, request):
        """Generate an HTTP MJPEG stream from the camera."""
        from haffmpeg.camera import CameraMjpeg

        _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)

        try:
            stream_reader = await stream.get_reader()
            return await async_aiohttp_proxy_stream(
                self.hass, request, stream_reader,
                ffmpeg_manager.ffmpeg_stream_content_type)
        finally:
            await stream.close()

    @property
    def supported_features(self):
        """Return supported features."""
        if self._input:
            return SUPPORT_STREAM
        return 0

    @property
    def stream_source(self):
        """Return the stream source."""
        return self._input

    @property
    def name(self):
        """Return the name of this camera."""
        return self._name
Esempio n. 7
0
class ONVIFCLI(Cmd):
    prompt = 'ONVIF >>> '
    client = None
    cmd_parser = None

    def setup(self, args):
        ''' `args`: Instance of `argparse.ArgumentParser` '''
        # Create onvif camera client
        self.client = ONVIFCamera(args.host, args.port,
                                  args.user, args.password,
                                  args.wsdl, encrypt=args.encrypt)


        # Create cmd argument parser
        self.create_cmd_parser()

    def create_cmd_parser(self):
        # Create parser to parse CMD, `params` is optional.
        cmd_parser = ThrowingArgumentParser(prog='ONVIF CMD',
                            usage='CMD service operation [params]')
        cmd_parser.add_argument('service')
        cmd_parser.add_argument('operation')
        cmd_parser.add_argument('params', default='{}', nargs=REMAINDER)
        self.cmd_parser = cmd_parser

    def do_cmd(self, line):
        '''Usage: CMD service operation [parameters]'''
        try:
            args = self.cmd_parser.parse_args(line.split())
        except ValueError as err:
            return error(err)

        # Check if args.service is valid
        if args.service not in SUPPORTED_SERVICES:
            return error('No Service: ' + args.service)

        args.params = ''.join(args.params)
        # params is optional
        if not args.params.strip():
            args.params = '{}'

        # params must be a dictionary format string
        match = re.match(r"^.*?(\{.*\}).*$", args.params)
        if not match:
            return error('Invalid params')

        try:
            args.params = dict(literal_eval(match.group(1)))
        except ValueError as err:
            return error('Invalid params')

        try:
            # Get ONVIF service
            service = self.client.get_service(args.service)
            # Actually execute the command and get the response
            response = getattr(service, args.operation)(args.params)
        except MethodNotFound as err:
            return error('No Operation: %s' % args.operation)
        except Exception as err:
            return error(err)

        if isinstance(response, (Text, bool)):
            return success(response)
        # Try to convert instance to dictionary
        try:
            success(ONVIFService.to_dict(response))
        except ONVIFError:
            error({})

    def complete_cmd(self, text, line, begidx, endidx):
        # TODO: complete service operations
        # service.ws_client.service._ServiceSelector__services[0].ports[0].methods.keys()
        if not text:
            completions = SUPPORTED_SERVICES[:]
        else:
            completions = [ key for key in SUPPORTED_SERVICES
                                if key.startswith(text) ]
        return completions

    def emptyline(self):
        return ''

    def do_EOF(self, line):
        return True