2020-02-20 23:29:46 +00:00
|
|
|
"""Support for ONVIF Cameras with FFmpeg as decoder."""
|
2017-06-16 05:28:17 +00:00
|
|
|
import asyncio
|
2019-05-03 17:01:12 +00:00
|
|
|
import datetime as dt
|
2017-06-16 05:28:17 +00:00
|
|
|
import logging
|
2018-03-16 03:30:41 +00:00
|
|
|
import os
|
2020-02-22 21:29:49 +00:00
|
|
|
from typing import Optional
|
2019-10-20 18:46:51 +00:00
|
|
|
|
|
|
|
from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError
|
|
|
|
from haffmpeg.camera import CameraMjpeg
|
|
|
|
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
|
|
|
import onvif
|
|
|
|
from onvif import ONVIFCamera, exceptions
|
2020-03-25 12:32:28 +00:00
|
|
|
import requests
|
|
|
|
from requests.auth import HTTPDigestAuth
|
2017-06-16 05:28:17 +00:00
|
|
|
import voluptuous as vol
|
2020-02-28 10:46:06 +00:00
|
|
|
from zeep.asyncio import AsyncTransport
|
2019-10-20 18:46:51 +00:00
|
|
|
from zeep.exceptions import Fault
|
2017-06-16 05:28:17 +00:00
|
|
|
|
2019-10-20 18:46:51 +00:00
|
|
|
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
|
|
|
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG
|
2017-06-16 05:28:17 +00:00
|
|
|
from homeassistant.const import (
|
2019-10-20 18:46:51 +00:00
|
|
|
ATTR_ENTITY_ID,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_HOST,
|
2019-10-20 18:46:51 +00:00
|
|
|
CONF_NAME,
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_PASSWORD,
|
|
|
|
CONF_PORT,
|
2019-10-20 18:46:51 +00:00
|
|
|
CONF_USERNAME,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-10-20 18:46:51 +00:00
|
|
|
from homeassistant.exceptions import PlatformNotReady
|
2020-02-28 10:46:06 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import (
|
|
|
|
async_aiohttp_proxy_stream,
|
|
|
|
async_get_clientsession,
|
|
|
|
)
|
2019-10-20 18:46:51 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2019-10-06 15:00:44 +00:00
|
|
|
from homeassistant.helpers.service import async_extract_entity_ids
|
2019-08-31 20:29:42 +00:00
|
|
|
import homeassistant.util.dt as dt_util
|
2017-06-16 05:28:17 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_NAME = "ONVIF Camera"
|
2017-06-16 05:28:17 +00:00
|
|
|
DEFAULT_PORT = 5000
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_USERNAME = "admin"
|
|
|
|
DEFAULT_PASSWORD = "888888"
|
|
|
|
DEFAULT_ARGUMENTS = "-pred 1"
|
2018-03-06 03:56:15 +00:00
|
|
|
DEFAULT_PROFILE = 0
|
|
|
|
|
|
|
|
CONF_PROFILE = "profile"
|
2020-03-06 17:59:57 +00:00
|
|
|
CONF_RTSP_TRANSPORT = "rtsp_transport"
|
2017-06-16 05:28:17 +00:00
|
|
|
|
2018-02-18 16:08:56 +00:00
|
|
|
ATTR_PAN = "pan"
|
|
|
|
ATTR_TILT = "tilt"
|
|
|
|
ATTR_ZOOM = "zoom"
|
2020-03-06 14:14:01 +00:00
|
|
|
ATTR_DISTANCE = "distance"
|
|
|
|
ATTR_SPEED = "speed"
|
|
|
|
ATTR_MOVE_MODE = "move_mode"
|
|
|
|
ATTR_CONTINUOUS_DURATION = "continuous_duration"
|
2018-02-18 16:08:56 +00:00
|
|
|
|
|
|
|
DIR_UP = "UP"
|
|
|
|
DIR_DOWN = "DOWN"
|
|
|
|
DIR_LEFT = "LEFT"
|
|
|
|
DIR_RIGHT = "RIGHT"
|
|
|
|
ZOOM_OUT = "ZOOM_OUT"
|
|
|
|
ZOOM_IN = "ZOOM_IN"
|
2020-03-06 14:14:01 +00:00
|
|
|
PAN_FACTOR = {DIR_RIGHT: 1, DIR_LEFT: -1}
|
|
|
|
TILT_FACTOR = {DIR_UP: 1, DIR_DOWN: -1}
|
|
|
|
ZOOM_FACTOR = {ZOOM_IN: 1, ZOOM_OUT: -1}
|
|
|
|
CONTINUOUS_MOVE = "ContinuousMove"
|
|
|
|
RELATIVE_MOVE = "RelativeMove"
|
|
|
|
ABSOLUTE_MOVE = "AbsoluteMove"
|
2018-02-18 16:08:56 +00:00
|
|
|
|
2020-03-06 14:14:01 +00:00
|
|
|
SERVICE_PTZ = "ptz"
|
2018-02-18 16:08:56 +00:00
|
|
|
|
2020-03-06 14:14:01 +00:00
|
|
|
DOMAIN = "onvif"
|
2018-02-18 16:08:56 +00:00
|
|
|
ONVIF_DATA = "onvif"
|
|
|
|
ENTITIES = "entities"
|
|
|
|
|
2020-03-06 17:59:57 +00:00
|
|
|
RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"]
|
2020-03-06 14:14:01 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_HOST): cv.string,
|
|
|
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
|
|
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
|
|
|
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
|
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
|
|
vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
|
2020-03-06 17:59:57 +00:00
|
|
|
vol.Optional(CONF_RTSP_TRANSPORT, default=RTSP_TRANS_PROTOCOLS[0]): vol.In(
|
|
|
|
RTSP_TRANS_PROTOCOLS
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): vol.All(
|
|
|
|
vol.Coerce(int), vol.Range(min=0)
|
|
|
|
),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
SERVICE_PTZ_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
ATTR_ENTITY_ID: cv.entity_ids,
|
2020-03-06 14:14:01 +00:00
|
|
|
vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]),
|
|
|
|
vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]),
|
|
|
|
vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]),
|
|
|
|
ATTR_MOVE_MODE: vol.In([CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE]),
|
|
|
|
vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float,
|
|
|
|
vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float,
|
|
|
|
vol.Optional(ATTR_SPEED, default=0.5): cv.small_float,
|
2019-07-31 19:25:30 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
2017-06-16 05:28:17 +00:00
|
|
|
"""Set up a ONVIF camera."""
|
2019-05-03 17:01:12 +00:00
|
|
|
_LOGGER.debug("Setting up the ONVIF camera platform")
|
|
|
|
|
|
|
|
async def async_handle_ptz(service):
|
2018-02-18 16:08:56 +00:00
|
|
|
"""Handle PTZ service call."""
|
2020-03-06 14:14:01 +00:00
|
|
|
pan = service.data.get(ATTR_PAN)
|
|
|
|
tilt = service.data.get(ATTR_TILT)
|
|
|
|
zoom = service.data.get(ATTR_ZOOM)
|
|
|
|
distance = service.data[ATTR_DISTANCE]
|
|
|
|
speed = service.data[ATTR_SPEED]
|
|
|
|
move_mode = service.data.get(ATTR_MOVE_MODE)
|
|
|
|
continuous_duration = service.data[ATTR_CONTINUOUS_DURATION]
|
2018-02-18 16:08:56 +00:00
|
|
|
all_cameras = hass.data[ONVIF_DATA][ENTITIES]
|
2019-10-06 15:00:44 +00:00
|
|
|
entity_ids = await async_extract_entity_ids(hass, service)
|
2018-02-18 16:08:56 +00:00
|
|
|
target_cameras = []
|
|
|
|
if not entity_ids:
|
|
|
|
target_cameras = all_cameras
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
target_cameras = [
|
|
|
|
camera for camera in all_cameras if camera.entity_id in entity_ids
|
|
|
|
]
|
2018-02-18 16:08:56 +00:00
|
|
|
for camera in target_cameras:
|
2020-03-06 14:14:01 +00:00
|
|
|
await camera.async_perform_ptz(
|
|
|
|
pan, tilt, zoom, distance, speed, move_mode, continuous_duration
|
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.services.async_register(
|
|
|
|
DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA
|
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
|
|
|
_LOGGER.debug("Constructing the ONVIFHassCamera")
|
|
|
|
|
|
|
|
hass_camera = ONVIFHassCamera(hass, config)
|
2018-02-18 16:08:56 +00:00
|
|
|
|
2019-05-03 17:01:12 +00:00
|
|
|
await hass_camera.async_initialize()
|
|
|
|
|
|
|
|
async_add_entities([hass_camera])
|
|
|
|
return
|
2017-06-16 05:28:17 +00:00
|
|
|
|
|
|
|
|
2018-02-17 13:57:05 +00:00
|
|
|
class ONVIFHassCamera(Camera):
|
2017-06-16 05:28:17 +00:00
|
|
|
"""An implementation of an ONVIF camera."""
|
|
|
|
|
|
|
|
def __init__(self, hass, config):
|
2019-05-03 17:01:12 +00:00
|
|
|
"""Initialize an ONVIF camera."""
|
2017-06-16 05:28:17 +00:00
|
|
|
super().__init__()
|
2019-05-03 17:01:12 +00:00
|
|
|
|
|
|
|
_LOGGER.debug("Importing dependencies")
|
|
|
|
|
|
|
|
_LOGGER.debug("Setting up the ONVIF camera component")
|
2017-06-16 05:28:17 +00:00
|
|
|
|
2018-03-16 03:30:41 +00:00
|
|
|
self._username = config.get(CONF_USERNAME)
|
|
|
|
self._password = config.get(CONF_PASSWORD)
|
|
|
|
self._host = config.get(CONF_HOST)
|
|
|
|
self._port = config.get(CONF_PORT)
|
2017-06-16 05:28:17 +00:00
|
|
|
self._name = config.get(CONF_NAME)
|
2018-01-16 08:23:48 +00:00
|
|
|
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
2018-03-16 03:30:41 +00:00
|
|
|
self._profile_index = config.get(CONF_PROFILE)
|
2019-05-03 17:01:12 +00:00
|
|
|
self._ptz_service = None
|
2018-02-17 13:57:05 +00:00
|
|
|
self._input = None
|
2020-03-24 16:57:14 +00:00
|
|
|
self._snapshot = None
|
2020-03-06 17:59:57 +00:00
|
|
|
self.stream_options[CONF_RTSP_TRANSPORT] = config.get(CONF_RTSP_TRANSPORT)
|
2020-02-22 21:29:49 +00:00
|
|
|
self._mac = None
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Setting up the ONVIF camera device @ '%s:%s'", self._host, self._port
|
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2020-02-28 10:46:06 +00:00
|
|
|
session = async_get_clientsession(hass)
|
|
|
|
transport = AsyncTransport(None, session=session)
|
2019-07-31 19:25:30 +00:00
|
|
|
self._camera = ONVIFCamera(
|
|
|
|
self._host,
|
|
|
|
self._port,
|
|
|
|
self._username,
|
|
|
|
self._password,
|
2020-04-07 21:14:28 +00:00
|
|
|
f"{os.path.dirname(onvif.__file__)}/wsdl/",
|
2020-02-28 10:46:06 +00:00
|
|
|
transport=transport,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
2020-02-22 21:29:49 +00:00
|
|
|
await self.async_obtain_mac_address()
|
2019-08-31 20:29:42 +00:00
|
|
|
await self.async_check_date_and_time()
|
|
|
|
await self.async_obtain_input_uri()
|
2020-03-24 16:57:14 +00:00
|
|
|
await self.async_obtain_snapshot_uri()
|
2019-08-31 20:29:42 +00:00
|
|
|
self.setup_ptz()
|
2019-09-23 17:41:35 +00:00
|
|
|
except ClientConnectionError as err:
|
2019-08-31 20:29:42 +00:00
|
|
|
_LOGGER.warning(
|
2019-10-20 18:46:51 +00:00
|
|
|
"Couldn't connect to camera '%s', but will retry later. Error: %s",
|
2019-08-31 20:29:42 +00:00
|
|
|
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,
|
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2020-02-22 21:29:49 +00:00
|
|
|
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
|
|
|
|
|
2019-08-31 20:29:42 +00:00
|
|
|
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()
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2019-08-31 20:29:42 +00:00
|
|
|
_LOGGER.debug("Retrieving current camera date/time")
|
|
|
|
try:
|
2019-05-03 17:01:12 +00:00
|
|
|
system_date = dt_util.utcnow()
|
|
|
|
device_time = await devicemgmt.GetSystemDateAndTime()
|
2019-10-24 04:03:25 +00:00
|
|
|
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
|
2019-05-17 04:29:52 +00:00
|
|
|
cdate = device_time.UTCDateTime
|
2019-10-24 04:03:25 +00:00
|
|
|
else:
|
|
|
|
tzone = (
|
|
|
|
dt_util.get_time_zone(device_time.TimeZone)
|
2019-10-24 20:39:10 +00:00
|
|
|
or dt_util.DEFAULT_TIME_ZONE
|
2019-10-24 04:03:25 +00:00
|
|
|
)
|
|
|
|
cdate = device_time.LocalDateTime
|
|
|
|
|
|
|
|
if cdate is None:
|
|
|
|
_LOGGER.warning("Could not retrieve date/time on this camera")
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
cam_date = dt.datetime(
|
|
|
|
cdate.Date.Year,
|
|
|
|
cdate.Date.Month,
|
|
|
|
cdate.Date.Day,
|
|
|
|
cdate.Time.Hour,
|
|
|
|
cdate.Time.Minute,
|
|
|
|
cdate.Time.Second,
|
|
|
|
0,
|
2019-10-24 04:03:25 +00:00
|
|
|
tzone,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-05-17 04:29:52 +00:00
|
|
|
|
2019-10-24 04:03:25 +00:00
|
|
|
cam_date_utc = cam_date.astimezone(dt_util.UTC)
|
|
|
|
|
|
|
|
_LOGGER.debug("TimeZone for date/time: %s", tzone)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Camera date/time: %s", cam_date)
|
2019-05-17 04:29:52 +00:00
|
|
|
|
2019-10-24 04:03:25 +00:00
|
|
|
_LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("System date/time: %s", system_date)
|
2019-05-17 04:29:52 +00:00
|
|
|
|
|
|
|
dt_diff = cam_date - system_date
|
|
|
|
dt_diff_seconds = dt_diff.total_seconds()
|
|
|
|
|
|
|
|
if dt_diff_seconds > 5:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
2019-10-24 04:03:25 +00:00
|
|
|
"The date/time on the camera (UTC) is '%s', "
|
2019-07-31 19:25:30 +00:00
|
|
|
"which is different from the system '%s', "
|
|
|
|
"this could lead to authentication issues",
|
2019-10-24 04:03:25 +00:00
|
|
|
cam_date_utc,
|
2019-07-31 19:25:30 +00:00
|
|
|
system_date,
|
|
|
|
)
|
2019-08-31 20:29:42 +00:00
|
|
|
except ServerDisconnectedError as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
2019-08-31 20:29:42 +00:00
|
|
|
"Couldn't get camera '%s' date/time. Error: %s", self._name, err
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2020-03-06 14:14:01 +00:00
|
|
|
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
|
|
|
|
|
2019-05-03 17:01:12 +00:00
|
|
|
async def async_obtain_input_uri(self):
|
2018-03-16 03:30:41 +00:00
|
|
|
"""Set the input uri for the camera."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Connecting with ONVIF Camera: %s on port %s", self._host, self._port
|
|
|
|
)
|
2018-03-16 03:30:41 +00:00
|
|
|
|
2018-02-17 13:57:05 +00:00
|
|
|
try:
|
2019-05-03 17:01:12 +00:00
|
|
|
_LOGGER.debug("Retrieving profiles")
|
|
|
|
|
|
|
|
media_service = self._camera.create_media_service()
|
|
|
|
|
|
|
|
profiles = await media_service.GetProfiles()
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Retrieved '%d' profiles", len(profiles))
|
2018-03-16 03:30:41 +00:00
|
|
|
|
|
|
|
if self._profile_index >= len(profiles):
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"ONVIF Camera '%s' doesn't provide profile %d."
|
|
|
|
" Using the last profile.",
|
|
|
|
self._name,
|
|
|
|
self._profile_index,
|
|
|
|
)
|
2018-03-06 03:56:15 +00:00
|
|
|
self._profile_index = -1
|
2018-03-16 03:30:41 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Using profile index '%d'", self._profile_index)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
|
|
|
_LOGGER.debug("Retrieving stream uri")
|
|
|
|
|
Fix Onvif setup error: premature end of connection on GetStreamURI (#26781)
* Fix Onvif setup error with a premature end of connection on GetStreamUri wsdl call
Reconnect to onvif camera after getting profiles to fix this error :
[homeassistant.components.onvif.camera] Retrieving stream uri
[zeep.asyncio.transport] HTTP Post to http://192.168.1.15/onvif/Media:
b'<?xml version=\'1.0\' encoding=\'utf-8\'?>\n<soap-env:Envelope xmlns:soap-env="http://www.w3.org/2003/05/soap-envelope"><soap-env:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecur
2019-09-20 01:08:51 ERROR (MainThread) [homeassistant.components.camera] Error while setting up platform onvif
Traceback (most recent call last):
File "/usr/local/lib/python3.7/site-packages/aiohttp/client_reqrep.py", line 553, in write_bytes
await self.body.write(writer)
File "/usr/local/lib/python3.7/site-packages/aiohttp/payload.py", line 231, in write
await writer.write(self._value)
File "/usr/local/lib/python3.7/site-packages/aiohttp/http_writer.py", line 101, in write
self._write(chunk)
File "/usr/local/lib/python3.7/site-packages/aiohttp/http_writer.py", line 67, in _write
raise ConnectionResetError('Cannot write to closing transport')
ConnectionResetError: Cannot write to closing transport
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 150, in _async_setup_platform
await asyncio.wait_for(asyncio.shield(task), SLOW_SETUP_MAX_WAIT)
File "/usr/local/lib/python3.7/asyncio/tasks.py", line 442, in wait_for
return fut.result()
File "/usr/src/homeassistant/homeassistant/components/onvif/camera.py", line 110, in async_setup_platform
await hass_camera.async_initialize()
File "/usr/src/homeassistant/homeassistant/components/onvif/camera.py", line 168, in async_initialize
await self.async_obtain_input_uri()
File "/usr/src/homeassistant/homeassistant/components/onvif/camera.py", line 266, in async_obtain_input_uri
stream_uri = await media_service.GetStreamUri(req)
File "/usr/local/lib/python3.7/site-packages/zeep/asyncio/bindings.py", line 13, in send
options["address"], envelope, http_headers
File "/usr/local/lib/python3.7/site-packages/zeep/asyncio/transport.py", line 107, in post_xml
response = await self.post(address, message, headers)
File "/usr/local/lib/python3.7/site-packages/zeep/asyncio/transport.py", line 95, in post
proxy=self.proxy,
File "/usr/local/lib/python3.7/site-packages/aiohttp/client.py", line 497, in _request
await resp.start(conn)
File "/usr/local/lib/python3.7/site-packages/aiohttp/client_reqrep.py", line 844, in start
message, payload = await self._protocol.read() # type: ignore # noqa
File "/usr/local/lib/python3.7/site-packages/aiohttp/streams.py", line 588, in read
await self._waiter
aiohttp.client_exceptions.ClientOSError: [Errno None] Can not write request body for http://192.168.1.15/onvif/Media
* Add code comment
* Update camera.py
* Lint.
2019-11-26 01:56:17 +00:00
|
|
|
# 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()
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
req = media_service.create_type("GetStreamUri")
|
2019-05-03 17:01:12 +00:00
|
|
|
req.ProfileToken = profiles[self._profile_index].token
|
2019-07-31 19:25:30 +00:00
|
|
|
req.StreamSetup = {
|
|
|
|
"Stream": "RTP-Unicast",
|
|
|
|
"Transport": {"Protocol": "RTSP"},
|
|
|
|
}
|
2018-03-16 03:30:41 +00:00
|
|
|
|
2019-05-03 17:01:12 +00:00
|
|
|
stream_uri = await media_service.GetStreamUri(req)
|
|
|
|
uri_no_auth = stream_uri.Uri
|
2019-07-31 19:25:30 +00:00
|
|
|
uri_for_log = uri_no_auth.replace("rtsp://", "rtsp://<user>:<password>@", 1)
|
2018-03-16 03:30:41 +00:00
|
|
|
self._input = uri_no_auth.replace(
|
2019-09-03 18:35:00 +00:00
|
|
|
"rtsp://", f"rtsp://{self._username}:{self._password}@", 1
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2018-02-17 13:57:05 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"ONVIF Camera Using the following URL for %s: %s",
|
2019-07-31 19:25:30 +00:00
|
|
|
self._name,
|
|
|
|
uri_for_log,
|
|
|
|
)
|
2018-02-18 16:08:56 +00:00
|
|
|
except exceptions.ONVIFError as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err)
|
2019-08-31 20:29:42 +00:00
|
|
|
|
2020-03-24 16:57:14 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
snapshot_uri = await media_service.GetSnapshotUri(req)
|
2020-03-25 12:32:28 +00:00
|
|
|
self._snapshot = snapshot_uri.Uri
|
2020-03-24 16:57:14 +00:00
|
|
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
"ONVIF Camera Using the following URL for %s snapshot: %s",
|
|
|
|
self._name,
|
2020-03-25 12:32:28 +00:00
|
|
|
self._snapshot,
|
2020-03-24 16:57:14 +00:00
|
|
|
)
|
|
|
|
except exceptions.ONVIFError as err:
|
|
|
|
_LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err)
|
|
|
|
|
2019-08-31 20:29:42 +00:00
|
|
|
def setup_ptz(self):
|
|
|
|
"""Set up PTZ if available."""
|
|
|
|
_LOGGER.debug("Setting up the ONVIF PTZ service")
|
2020-03-13 22:58:14 +00:00
|
|
|
if self._camera.get_service("ptz", create=False) is None:
|
2019-09-18 16:00:12 +00:00
|
|
|
_LOGGER.debug("PTZ is not available")
|
2019-08-31 20:29:42 +00:00
|
|
|
else:
|
|
|
|
self._ptz_service = self._camera.create_ptz_service()
|
2020-03-06 14:14:01 +00:00
|
|
|
_LOGGER.debug("Completed set up of the ONVIF camera component")
|
2018-02-18 16:08:56 +00:00
|
|
|
|
2020-03-06 14:14:01 +00:00
|
|
|
async def async_perform_ptz(
|
|
|
|
self, pan, tilt, zoom, distance, speed, move_mode, continuous_duration
|
|
|
|
):
|
2018-02-18 16:08:56 +00:00
|
|
|
"""Perform a PTZ action on the camera."""
|
2019-05-03 17:01:12 +00:00
|
|
|
if self._ptz_service is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name)
|
2019-05-03 17:01:12 +00:00
|
|
|
return
|
|
|
|
|
2018-03-16 03:30:41 +00:00
|
|
|
if self._ptz_service:
|
2020-03-06 14:14:01 +00:00
|
|
|
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,
|
|
|
|
)
|
2018-03-16 03:30:41 +00:00
|
|
|
try:
|
2020-03-06 14:14:01 +00:00
|
|
|
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)
|
2018-03-16 03:30:41 +00:00
|
|
|
except exceptions.ONVIFError as err:
|
|
|
|
if "Bad Request" in err.reason:
|
|
|
|
self._ptz_service = None
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
|
2018-03-16 03:30:41 +00:00
|
|
|
else:
|
|
|
|
_LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name)
|
2018-02-18 16:08:56 +00:00
|
|
|
|
2018-03-16 03:30:41 +00:00
|
|
|
async def async_added_to_hass(self):
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Handle entity addition to hass."""
|
2019-05-03 17:01:12 +00:00
|
|
|
_LOGGER.debug("Camera '%s' added to hass", self._name)
|
|
|
|
|
2018-02-18 16:08:56 +00:00
|
|
|
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)
|
2017-06-16 05:28:17 +00:00
|
|
|
|
2018-03-16 03:30:41 +00:00
|
|
|
async def async_camera_image(self):
|
2017-06-16 05:28:17 +00:00
|
|
|
"""Return a still image response from the camera."""
|
2019-05-03 17:01:12 +00:00
|
|
|
_LOGGER.debug("Retrieving image from camera '%s'", self._name)
|
2020-03-25 12:32:28 +00:00
|
|
|
image = None
|
2018-03-16 03:30:41 +00:00
|
|
|
|
2020-03-24 16:57:14 +00:00
|
|
|
if self._snapshot is not None:
|
2020-03-25 12:32:28 +00:00
|
|
|
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)
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
|
|
|
image = await self.hass.async_add_job(fetch)
|
2020-03-24 16:57:14 +00:00
|
|
|
|
2020-03-25 12:32:28 +00:00
|
|
|
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)
|
2020-03-24 16:57:14 +00:00
|
|
|
image = await asyncio.shield(
|
|
|
|
ffmpeg.get_image(
|
|
|
|
self._input,
|
|
|
|
output_format=IMAGE_JPEG,
|
|
|
|
extra_cmd=self._ffmpeg_arguments,
|
|
|
|
)
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2020-03-24 16:57:14 +00:00
|
|
|
|
2017-06-16 05:28:17 +00:00
|
|
|
return image
|
|
|
|
|
2018-03-16 03:30:41 +00:00
|
|
|
async def handle_async_mjpeg_stream(self, request):
|
2017-06-16 05:28:17 +00:00
|
|
|
"""Generate an HTTP MJPEG stream from the camera."""
|
2019-05-03 17:01:12 +00:00
|
|
|
_LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name)
|
2018-03-16 03:30:41 +00:00
|
|
|
|
2019-02-04 17:57:22 +00:00
|
|
|
ffmpeg_manager = self.hass.data[DATA_FFMPEG]
|
2019-07-31 19:25:30 +00:00
|
|
|
stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop)
|
2019-05-03 17:01:12 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
await stream.open_camera(self._input, extra_cmd=self._ffmpeg_arguments)
|
2017-06-16 05:28:17 +00:00
|
|
|
|
2018-11-01 08:28:23 +00:00
|
|
|
try:
|
2019-03-27 06:55:05 +00:00
|
|
|
stream_reader = await stream.get_reader()
|
2018-11-01 08:28:23 +00:00
|
|
|
return await async_aiohttp_proxy_stream(
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass,
|
|
|
|
request,
|
|
|
|
stream_reader,
|
|
|
|
ffmpeg_manager.ffmpeg_stream_content_type,
|
|
|
|
)
|
2018-11-01 08:28:23 +00:00
|
|
|
finally:
|
|
|
|
await stream.close()
|
2017-06-16 05:28:17 +00:00
|
|
|
|
2019-03-31 22:12:55 +00:00
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Return supported features."""
|
|
|
|
if self._input:
|
|
|
|
return SUPPORT_STREAM
|
|
|
|
return 0
|
|
|
|
|
2019-05-23 16:45:30 +00:00
|
|
|
async def stream_source(self):
|
2019-03-31 22:12:55 +00:00
|
|
|
"""Return the stream source."""
|
|
|
|
return self._input
|
|
|
|
|
2017-06-16 05:28:17 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of this camera."""
|
|
|
|
return self._name
|
2020-02-22 21:29:49 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self) -> Optional[str]:
|
|
|
|
"""Return a unique ID."""
|
2020-03-21 18:36:35 +00:00
|
|
|
if self._profile_index:
|
|
|
|
return f"{self._mac}_{self._profile_index}"
|
2020-02-22 21:29:49 +00:00
|
|
|
return self._mac
|