185 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			185 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Python
		
	
	
"""Support for IP Cameras."""
 | 
						|
import asyncio
 | 
						|
import logging
 | 
						|
 | 
						|
import aiohttp
 | 
						|
import async_timeout
 | 
						|
import requests
 | 
						|
from requests.auth import HTTPDigestAuth
 | 
						|
import voluptuous as vol
 | 
						|
 | 
						|
from homeassistant.components.camera import (
 | 
						|
    DEFAULT_CONTENT_TYPE,
 | 
						|
    PLATFORM_SCHEMA,
 | 
						|
    SUPPORT_STREAM,
 | 
						|
    Camera,
 | 
						|
)
 | 
						|
from homeassistant.const import (
 | 
						|
    CONF_AUTHENTICATION,
 | 
						|
    CONF_NAME,
 | 
						|
    CONF_PASSWORD,
 | 
						|
    CONF_USERNAME,
 | 
						|
    CONF_VERIFY_SSL,
 | 
						|
    HTTP_BASIC_AUTHENTICATION,
 | 
						|
    HTTP_DIGEST_AUTHENTICATION,
 | 
						|
)
 | 
						|
from homeassistant.exceptions import TemplateError
 | 
						|
from homeassistant.helpers import config_validation as cv
 | 
						|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
 | 
						|
from homeassistant.helpers.reload import async_setup_reload_service
 | 
						|
 | 
						|
from . import DOMAIN, PLATFORMS
 | 
						|
 | 
						|
_LOGGER = logging.getLogger(__name__)
 | 
						|
 | 
						|
CONF_CONTENT_TYPE = "content_type"
 | 
						|
CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change"
 | 
						|
CONF_STILL_IMAGE_URL = "still_image_url"
 | 
						|
CONF_STREAM_SOURCE = "stream_source"
 | 
						|
CONF_FRAMERATE = "framerate"
 | 
						|
 | 
						|
DEFAULT_NAME = "Generic Camera"
 | 
						|
 | 
						|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
 | 
						|
    {
 | 
						|
        vol.Required(CONF_STILL_IMAGE_URL): cv.template,
 | 
						|
        vol.Optional(CONF_STREAM_SOURCE): cv.template,
 | 
						|
        vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In(
 | 
						|
            [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
 | 
						|
        ),
 | 
						|
        vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean,
 | 
						|
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
 | 
						|
        vol.Optional(CONF_PASSWORD): cv.string,
 | 
						|
        vol.Optional(CONF_USERNAME): cv.string,
 | 
						|
        vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string,
 | 
						|
        vol.Optional(CONF_FRAMERATE, default=2): vol.Any(
 | 
						|
            cv.small_float, cv.positive_int
 | 
						|
        ),
 | 
						|
        vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
 | 
						|
    }
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
 | 
						|
    """Set up a generic IP Camera."""
 | 
						|
 | 
						|
    await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
 | 
						|
 | 
						|
    async_add_entities([GenericCamera(hass, config)])
 | 
						|
 | 
						|
 | 
						|
class GenericCamera(Camera):
 | 
						|
    """A generic implementation of an IP camera."""
 | 
						|
 | 
						|
    def __init__(self, hass, device_info):
 | 
						|
        """Initialize a generic camera."""
 | 
						|
        super().__init__()
 | 
						|
        self.hass = hass
 | 
						|
        self._authentication = device_info.get(CONF_AUTHENTICATION)
 | 
						|
        self._name = device_info.get(CONF_NAME)
 | 
						|
        self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
 | 
						|
        self._stream_source = device_info.get(CONF_STREAM_SOURCE)
 | 
						|
        self._still_image_url.hass = hass
 | 
						|
        if self._stream_source is not None:
 | 
						|
            self._stream_source.hass = hass
 | 
						|
        self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
 | 
						|
        self._frame_interval = 1 / device_info[CONF_FRAMERATE]
 | 
						|
        self._supported_features = SUPPORT_STREAM if self._stream_source else 0
 | 
						|
        self.content_type = device_info[CONF_CONTENT_TYPE]
 | 
						|
        self.verify_ssl = device_info[CONF_VERIFY_SSL]
 | 
						|
 | 
						|
        username = device_info.get(CONF_USERNAME)
 | 
						|
        password = device_info.get(CONF_PASSWORD)
 | 
						|
 | 
						|
        if username and password:
 | 
						|
            if self._authentication == HTTP_DIGEST_AUTHENTICATION:
 | 
						|
                self._auth = HTTPDigestAuth(username, password)
 | 
						|
            else:
 | 
						|
                self._auth = aiohttp.BasicAuth(username, password=password)
 | 
						|
        else:
 | 
						|
            self._auth = None
 | 
						|
 | 
						|
        self._last_url = None
 | 
						|
        self._last_image = None
 | 
						|
 | 
						|
    @property
 | 
						|
    def supported_features(self):
 | 
						|
        """Return supported features for this camera."""
 | 
						|
        return self._supported_features
 | 
						|
 | 
						|
    @property
 | 
						|
    def frame_interval(self):
 | 
						|
        """Return the interval between frames of the mjpeg stream."""
 | 
						|
        return self._frame_interval
 | 
						|
 | 
						|
    def camera_image(self):
 | 
						|
        """Return bytes of camera image."""
 | 
						|
        return asyncio.run_coroutine_threadsafe(
 | 
						|
            self.async_camera_image(), self.hass.loop
 | 
						|
        ).result()
 | 
						|
 | 
						|
    async def async_camera_image(self):
 | 
						|
        """Return a still image response from the camera."""
 | 
						|
        try:
 | 
						|
            url = self._still_image_url.async_render(parse_result=False)
 | 
						|
        except TemplateError as err:
 | 
						|
            _LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
 | 
						|
            return self._last_image
 | 
						|
 | 
						|
        if url == self._last_url and self._limit_refetch:
 | 
						|
            return self._last_image
 | 
						|
 | 
						|
        # aiohttp don't support DigestAuth yet
 | 
						|
        if self._authentication == HTTP_DIGEST_AUTHENTICATION:
 | 
						|
 | 
						|
            def fetch():
 | 
						|
                """Read image from a URL."""
 | 
						|
                try:
 | 
						|
                    response = requests.get(
 | 
						|
                        url, timeout=10, auth=self._auth, verify=self.verify_ssl
 | 
						|
                    )
 | 
						|
                    return response.content
 | 
						|
                except requests.exceptions.RequestException as error:
 | 
						|
                    _LOGGER.error(
 | 
						|
                        "Error getting new camera image from %s: %s", self._name, error
 | 
						|
                    )
 | 
						|
                    return self._last_image
 | 
						|
 | 
						|
            self._last_image = await self.hass.async_add_executor_job(fetch)
 | 
						|
        # async
 | 
						|
        else:
 | 
						|
            try:
 | 
						|
                websession = async_get_clientsession(
 | 
						|
                    self.hass, verify_ssl=self.verify_ssl
 | 
						|
                )
 | 
						|
                with async_timeout.timeout(10):
 | 
						|
                    response = await websession.get(url, auth=self._auth)
 | 
						|
                self._last_image = await response.read()
 | 
						|
            except asyncio.TimeoutError:
 | 
						|
                _LOGGER.error("Timeout getting camera image from %s", self._name)
 | 
						|
                return self._last_image
 | 
						|
            except aiohttp.ClientError as err:
 | 
						|
                _LOGGER.error(
 | 
						|
                    "Error getting new camera image from %s: %s", self._name, err
 | 
						|
                )
 | 
						|
                return self._last_image
 | 
						|
 | 
						|
        self._last_url = url
 | 
						|
        return self._last_image
 | 
						|
 | 
						|
    @property
 | 
						|
    def name(self):
 | 
						|
        """Return the name of this device."""
 | 
						|
        return self._name
 | 
						|
 | 
						|
    async def stream_source(self):
 | 
						|
        """Return the source of the stream."""
 | 
						|
        if self._stream_source is None:
 | 
						|
            return None
 | 
						|
 | 
						|
        try:
 | 
						|
            return self._stream_source.async_render(parse_result=False)
 | 
						|
        except TemplateError as err:
 | 
						|
            _LOGGER.error("Error parsing template %s: %s", self._stream_source, err)
 | 
						|
            return None
 |