Pass width and height when requesting camera snapshot (#53835)

This commit is contained in:
J. Nick Koston 2021-08-10 19:33:06 -05:00 committed by GitHub
parent 390023a576
commit e99576c094
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 418 additions and 113 deletions

View file

@ -1,4 +1,6 @@
"""Support for Abode Security System cameras.""" """Support for Abode Security System cameras."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import abodepy.helpers.constants as CONST import abodepy.helpers.constants as CONST
@ -73,7 +75,9 @@ class AbodeCamera(AbodeDevice, Camera):
else: else:
self._response = None self._response = None
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Get a camera image.""" """Get a camera image."""
self.refresh_image() self.refresh_image()

View file

@ -1,4 +1,6 @@
"""Support for Amcrest IP cameras.""" """Support for Amcrest IP cameras."""
from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from functools import partial from functools import partial
@ -181,7 +183,9 @@ class AmcrestCam(Camera):
finally: finally:
self._snapshot_task = None self._snapshot_task = None
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
_LOGGER.debug("Take snapshot from %s", self._name) _LOGGER.debug("Take snapshot from %s", self._name)
try: try:

View file

@ -1,4 +1,6 @@
"""Support for Netgear Arlo IP cameras.""" """Support for Netgear Arlo IP cameras."""
from __future__ import annotations
import logging import logging
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
@ -62,7 +64,9 @@ class ArloCam(Camera):
self._last_refresh = None self._last_refresh = None
self.attrs = {} self.attrs = {}
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
return self._camera.last_image_from_cache return self._camera.last_image_from_cache

View file

@ -1,4 +1,5 @@
"""Support for August doorbell camera.""" """Support for August doorbell camera."""
from __future__ import annotations
from yalexs.activity import ActivityType from yalexs.activity import ActivityType
from yalexs.util import update_doorbell_image_from_activity from yalexs.util import update_doorbell_image_from_activity
@ -68,7 +69,9 @@ class AugustCamera(AugustEntityMixin, Camera):
if doorbell_activity is not None: if doorbell_activity is not None:
update_doorbell_image_from_activity(self._detail, doorbell_activity) update_doorbell_image_from_activity(self._detail, doorbell_activity)
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
self._update_from_data() self._update_from_data()

View file

@ -1,4 +1,6 @@
"""Support for Blink system camera.""" """Support for Blink system camera."""
from __future__ import annotations
import logging import logging
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
@ -65,6 +67,8 @@ class BlinkCamera(Camera):
self._camera.snap_picture() self._camera.snap_picture()
self.data.refresh() self.data.refresh()
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
return self._camera.image_from_cache.content return self._camera.image_from_cache.content

View file

@ -1,4 +1,6 @@
"""Support for a camera of a BloomSky weather station.""" """Support for a camera of a BloomSky weather station."""
from __future__ import annotations
import logging import logging
import requests import requests
@ -37,7 +39,9 @@ class BloomSkyCamera(Camera):
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self._attr_unique_id = self._id self._attr_unique_id = self._id
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Update the camera's image if it has changed.""" """Update the camera's image if it has changed."""
try: try:
self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"]

View file

@ -143,7 +143,9 @@ class BuienradarCam(Camera):
_LOGGER.error("Failed to fetch image, %s", type(err)) _LOGGER.error("Failed to fetch image, %s", type(err))
return False return False
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
""" """
Return a still image response from the camera. Return a still image response from the camera.

View file

@ -8,7 +8,9 @@ from collections.abc import Awaitable, Mapping
from contextlib import suppress from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial
import hashlib import hashlib
import inspect
import logging import logging
import os import os
from random import SystemRandom from random import SystemRandom
@ -62,6 +64,7 @@ from .const import (
DOMAIN, DOMAIN,
SERVICE_RECORD, SERVICE_RECORD,
) )
from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences from .prefs import CameraPreferences
# mypy: allow-untyped-calls # mypy: allow-untyped-calls
@ -138,23 +141,72 @@ async def async_request_stream(hass: HomeAssistant, entity_id: str, fmt: str) ->
return await _async_stream_endpoint_url(hass, camera, fmt) return await _async_stream_endpoint_url(hass, camera, fmt)
@bind_hass async def _async_get_image(
async def async_get_image( camera: Camera,
hass: HomeAssistant, entity_id: str, timeout: int = 10 timeout: int = 10,
width: int | None = None,
height: int | None = None,
) -> Image: ) -> Image:
"""Fetch an image from a camera entity.""" """Fetch a snapshot image from a camera.
camera = _get_camera_from_entity_id(hass, entity_id)
If width and height are passed, an attempt to scale
the image will be made on a best effort basis.
Not all cameras can scale images or return jpegs
that we can scale, however the majority of cases
are handled.
"""
with suppress(asyncio.CancelledError, asyncio.TimeoutError): with suppress(asyncio.CancelledError, asyncio.TimeoutError):
async with async_timeout.timeout(timeout): async with async_timeout.timeout(timeout):
image = await camera.async_camera_image() # Calling inspect will be removed in 2022.1 after all
# custom components have had a chance to change their signature
sig = inspect.signature(camera.async_camera_image)
if "height" in sig.parameters and "width" in sig.parameters:
image_bytes = await camera.async_camera_image(
width=width, height=height
)
else:
_LOGGER.warning(
"The camera entity %s does not support requesting width and height, please open an issue with the integration author",
camera.entity_id,
)
image_bytes = await camera.async_camera_image()
if image: if image_bytes:
return Image(camera.content_type, image) content_type = camera.content_type
image = Image(content_type, image_bytes)
if (
width is not None
and height is not None
and "jpeg" in content_type
or "jpg" in content_type
):
assert width is not None
assert height is not None
return Image(
content_type, scale_jpeg_camera_image(image, width, height)
)
return image
raise HomeAssistantError("Unable to get image") raise HomeAssistantError("Unable to get image")
@bind_hass
async def async_get_image(
hass: HomeAssistant,
entity_id: str,
timeout: int = 10,
width: int | None = None,
height: int | None = None,
) -> Image:
"""Fetch an image from a camera entity.
width and height will be passed to the underlying camera.
"""
camera = _get_camera_from_entity_id(hass, entity_id)
return await _async_get_image(camera, timeout, width, height)
@bind_hass @bind_hass
async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None:
"""Fetch the stream source for a camera entity.""" """Fetch the stream source for a camera entity."""
@ -387,12 +439,27 @@ class Camera(Entity):
"""Return the source of the stream.""" """Return the source of the stream."""
return None return None
def camera_image(self) -> bytes | None: def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
sig = inspect.signature(self.camera_image)
# Calling inspect will be removed in 2022.1 after all
# custom components have had a chance to change their signature
if "height" in sig.parameters and "width" in sig.parameters:
return await self.hass.async_add_executor_job(
partial(self.camera_image, width=width, height=height)
)
_LOGGER.warning(
"The camera entity %s does not support requesting width and height, please open an issue with the integration author",
self.entity_id,
)
return await self.hass.async_add_executor_job(self.camera_image) return await self.hass.async_add_executor_job(self.camera_image)
async def handle_async_still_stream( async def handle_async_still_stream(
@ -529,14 +596,19 @@ class CameraImageView(CameraView):
async def handle(self, request: web.Request, camera: Camera) -> web.Response: async def handle(self, request: web.Request, camera: Camera) -> web.Response:
"""Serve camera image.""" """Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError): width = request.query.get("width")
async with async_timeout.timeout(CAMERA_IMAGE_TIMEOUT): height = request.query.get("height")
image = await camera.async_camera_image() try:
image = await _async_get_image(
if image: camera,
return web.Response(body=image, content_type=camera.content_type) CAMERA_IMAGE_TIMEOUT,
int(width) if width else None,
raise web.HTTPInternalServerError() int(height) if height else None,
)
except (HomeAssistantError, ValueError) as ex:
raise web.HTTPInternalServerError() from ex
else:
return web.Response(body=image.content, content_type=image.content_type)
class CameraMjpegStream(CameraView): class CameraMjpegStream(CameraView):

View file

@ -1,19 +1,32 @@
"""Image processing for HomeKit component.""" """Image processing for cameras."""
import logging import logging
from typing import TYPE_CHECKING, cast
SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
JPEG_QUALITY = 75
def scale_jpeg_camera_image(cam_image, width, height): if TYPE_CHECKING:
from turbojpeg import TurboJPEG
from . import Image
def scale_jpeg_camera_image(cam_image: "Image", width: int, height: int) -> bytes:
"""Scale a camera image as close as possible to one of the supported scaling factors.""" """Scale a camera image as close as possible to one of the supported scaling factors."""
turbo_jpeg = TurboJPEGSingleton.instance() turbo_jpeg = TurboJPEGSingleton.instance()
if not turbo_jpeg: if not turbo_jpeg:
return cam_image.content return cam_image.content
(current_width, current_height, _, _) = turbo_jpeg.decode_header(cam_image.content) try:
(current_width, current_height, _, _) = turbo_jpeg.decode_header(
cam_image.content
)
except OSError:
return cam_image.content
if current_width <= width or current_height <= height: if current_width <= width or current_height <= height:
return cam_image.content return cam_image.content
@ -26,10 +39,13 @@ def scale_jpeg_camera_image(cam_image, width, height):
scaling_factor = supported_sf scaling_factor = supported_sf
break break
return turbo_jpeg.scale_with_quality( return cast(
bytes,
turbo_jpeg.scale_with_quality(
cam_image.content, cam_image.content,
scaling_factor=scaling_factor, scaling_factor=scaling_factor,
quality=75, quality=JPEG_QUALITY,
),
) )
@ -45,13 +61,13 @@ class TurboJPEGSingleton:
__instance = None __instance = None
@staticmethod @staticmethod
def instance(): def instance() -> "TurboJPEG":
"""Singleton for TurboJPEG.""" """Singleton for TurboJPEG."""
if TurboJPEGSingleton.__instance is None: if TurboJPEGSingleton.__instance is None:
TurboJPEGSingleton() TurboJPEGSingleton()
return TurboJPEGSingleton.__instance return TurboJPEGSingleton.__instance
def __init__(self): def __init__(self) -> None:
"""Try to create TurboJPEG only once.""" """Try to create TurboJPEG only once."""
# pylint: disable=unused-private-member # pylint: disable=unused-private-member
# https://github.com/PyCQA/pylint/issues/4681 # https://github.com/PyCQA/pylint/issues/4681

View file

@ -3,6 +3,7 @@
"name": "Camera", "name": "Camera",
"documentation": "https://www.home-assistant.io/integrations/camera", "documentation": "https://www.home-assistant.io/integrations/camera",
"dependencies": ["http"], "dependencies": ["http"],
"requirements": ["PyTurboJPEG==1.5.0"],
"after_dependencies": ["media_player"], "after_dependencies": ["media_player"],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"

View file

@ -123,7 +123,9 @@ class CanaryCamera(CoordinatorEntity, Camera):
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return not self.location.is_recording return not self.location.is_recording
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
await self.hass.async_add_executor_job(self.renew_live_stream_session) await self.hass.async_add_executor_job(self.renew_live_stream_session)
live_stream_url = await self.hass.async_add_executor_job( live_stream_url = await self.hass.async_add_executor_job(

View file

@ -1,4 +1,6 @@
"""Demo camera platform that has a fake camera.""" """Demo camera platform that has a fake camera."""
from __future__ import annotations
from pathlib import Path from pathlib import Path
from homeassistant.components.camera import SUPPORT_ON_OFF, Camera from homeassistant.components.camera import SUPPORT_ON_OFF, Camera
@ -25,7 +27,9 @@ class DemoCamera(Camera):
self.is_streaming = True self.is_streaming = True
self._images_index = 0 self._images_index = 0
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes:
"""Return a faked still image response.""" """Return a faked still image response."""
self._images_index = (self._images_index + 1) % 4 self._images_index = (self._images_index + 1) % 4
image_path = Path(__file__).parent / f"demo_{self._images_index}.jpg" image_path = Path(__file__).parent / f"demo_{self._images_index}.jpg"

View file

@ -1,4 +1,6 @@
"""Support for viewing the camera feed from a DoorBird video doorbell.""" """Support for viewing the camera feed from a DoorBird video doorbell."""
from __future__ import annotations
import asyncio import asyncio
import datetime import datetime
import logging import logging
@ -112,7 +114,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
"""Get the name of the camera.""" """Get the name of the camera."""
return self._name return self._name
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Pull a still image from the camera.""" """Pull a still image from the camera."""
now = dt_util.utcnow() now = dt_util.utcnow()

View file

@ -1,4 +1,6 @@
"""Support for the Environment Canada radar imagery.""" """Support for the Environment Canada radar imagery."""
from __future__ import annotations
import datetime import datetime
from env_canada import ECRadar from env_canada import ECRadar
@ -68,7 +70,9 @@ class ECCamera(Camera):
self.image = None self.image = None
self.timestamp = None self.timestamp = None
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
self.update() self.update()
return self.image return self.image

View file

@ -50,7 +50,9 @@ class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]):
async with self._image_cond: async with self._image_cond:
self._image_cond.notify_all() self._image_cond.notify_all()
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return single camera image bytes.""" """Return single camera image bytes."""
if not self.available: if not self.available:
return None return None

View file

@ -325,7 +325,9 @@ class EzvizCamera(CoordinatorEntity, Camera):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._serial return self._serial
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a frame from the camera stream.""" """Return a frame from the camera stream."""
ffmpeg = ImageFrame(self._ffmpeg.binary) ffmpeg = ImageFrame(self._ffmpeg.binary)

View file

@ -1,4 +1,6 @@
"""Family Hub camera for Samsung Refrigerators.""" """Family Hub camera for Samsung Refrigerators."""
from __future__ import annotations
from pyfamilyhublocal import FamilyHubCam from pyfamilyhublocal import FamilyHubCam
import voluptuous as vol import voluptuous as vol
@ -38,7 +40,9 @@ class FamilyHubCamera(Camera):
self._name = name self._name = name
self.family_hub_cam = family_hub_cam self.family_hub_cam = family_hub_cam
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response.""" """Return a still image response."""
return await self.family_hub_cam.async_get_cam_image() return await self.family_hub_cam.async_get_cam_image()

View file

@ -1,4 +1,5 @@
"""Support for Cameras with FFmpeg as decoder.""" """Support for Cameras with FFmpeg as decoder."""
from __future__ import annotations
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG from haffmpeg.tools import IMAGE_JPEG
@ -49,7 +50,9 @@ class FFmpegCamera(Camera):
"""Return the stream source.""" """Return the stream source."""
return self._input.split(" ")[-1] return self._input.split(" ")[-1]
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
return await async_get_image( return await async_get_image(
self.hass, self.hass,

View file

@ -1,4 +1,6 @@
"""This component provides basic support for Foscam IP cameras.""" """This component provides basic support for Foscam IP cameras."""
from __future__ import annotations
import asyncio import asyncio
from libpyfoscam import FoscamCamera from libpyfoscam import FoscamCamera
@ -172,7 +174,9 @@ class HassFoscamCamera(Camera):
"""Return the entity unique ID.""" """Return the entity unique ID."""
return self._unique_id return self._unique_id
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
# Send the request to snap a picture and return raw jpg data # Send the request to snap a picture and return raw jpg data
# Handle exception if host is not reachable or url failed # Handle exception if host is not reachable or url failed

View file

@ -1,4 +1,6 @@
"""Support for IP Cameras.""" """Support for IP Cameras."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
@ -118,13 +120,17 @@ class GenericCamera(Camera):
"""Return the interval between frames of the mjpeg stream.""" """Return the interval between frames of the mjpeg stream."""
return self._frame_interval return self._frame_interval
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
return asyncio.run_coroutine_threadsafe( return asyncio.run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop self.async_camera_image(), self.hass.loop
).result() ).result()
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
try: try:
url = self._still_image_url.async_render(parse_result=False) url = self._still_image_url.async_render(parse_result=False)

View file

@ -6,8 +6,7 @@
"HAP-python==4.0.0", "HAP-python==4.0.0",
"fnvhash==0.1.0", "fnvhash==0.1.0",
"PyQRCode==1.2.1", "PyQRCode==1.2.1",
"base36==0.1.1", "base36==0.1.1"
"PyTurboJPEG==1.5.0"
], ],
"dependencies": ["http", "camera", "ffmpeg", "network"], "dependencies": ["http", "camera", "ffmpeg", "network"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],

View file

@ -55,7 +55,6 @@ from .const import (
SERV_SPEAKER, SERV_SPEAKER,
SERV_STATELESS_PROGRAMMABLE_SWITCH, SERV_STATELESS_PROGRAMMABLE_SWITCH,
) )
from .img_util import scale_jpeg_camera_image
from .util import pid_is_alive from .util import pid_is_alive
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -467,8 +466,9 @@ class Camera(HomeAccessory, PyhapCamera):
async def async_get_snapshot(self, image_size): async def async_get_snapshot(self, image_size):
"""Return a jpeg of a snapshot from the camera.""" """Return a jpeg of a snapshot from the camera."""
return scale_jpeg_camera_image( image = await self.hass.components.camera.async_get_image(
await self.hass.components.camera.async_get_image(self.entity_id), self.entity_id,
image_size["image-width"], width=image_size["image-width"],
image_size["image-height"], height=image_size["image-height"],
) )
return image.content

View file

@ -1,4 +1,6 @@
"""Support for Homekit cameras.""" """Support for Homekit cameras."""
from __future__ import annotations
from aiohomekit.model.services import ServicesTypes from aiohomekit.model.services import ServicesTypes
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
@ -21,12 +23,14 @@ class HomeKitCamera(AccessoryEntity, Camera):
"""Return the current state of the camera.""" """Return the current state of the camera."""
return "idle" return "idle"
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a jpeg with the current camera snapshot.""" """Return a jpeg with the current camera snapshot."""
return await self._accessory.pairing.image( return await self._accessory.pairing.image(
self._aid, self._aid,
640, width or 640,
480, height or 480,
) )

View file

@ -210,7 +210,9 @@ class HyperionCamera(Camera):
finally: finally:
await self._stop_image_streaming_for_client() await self._stop_image_streaming_for_client()
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return single camera image bytes.""" """Return single camera image bytes."""
async with self._image_streaming() as is_streaming: async with self._image_streaming() as is_streaming:
if is_streaming: if is_streaming:

View file

@ -1,4 +1,6 @@
"""Camera that loads a picture from a local file.""" """Camera that loads a picture from a local file."""
from __future__ import annotations
import logging import logging
import mimetypes import mimetypes
import os import os
@ -73,7 +75,9 @@ class LocalFile(Camera):
if content is not None: if content is not None:
self.content_type = content self.content_type = content
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response.""" """Return image response."""
try: try:
with open(self._file_path, "rb") as file: with open(self._file_path, "rb") as file:
@ -84,6 +88,7 @@ class LocalFile(Camera):
self._name, self._name,
self._file_path, self._file_path,
) )
return None
def check_file_path_access(self, file_path): def check_file_path_access(self, file_path):
"""Check that filepath given is readable.""" """Check that filepath given is readable."""

View file

@ -1,4 +1,6 @@
"""Support to the Logi Circle cameras.""" """Support to the Logi Circle cameras."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -142,7 +144,9 @@ class LogiCam(Camera):
return state return state
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image from the camera.""" """Return a still image from the camera."""
return await self._camera.live_stream.download_jpeg() return await self._camera.live_stream.download_jpeg()

View file

@ -1,4 +1,6 @@
"""Support for IP Cameras.""" """Support for IP Cameras."""
from __future__ import annotations
import asyncio import asyncio
from contextlib import closing from contextlib import closing
import logging import logging
@ -106,7 +108,9 @@ class MjpegCamera(Camera):
self._auth = aiohttp.BasicAuth(self._username, password=self._password) self._auth = aiohttp.BasicAuth(self._username, password=self._password)
self._verify_ssl = device_info.get(CONF_VERIFY_SSL) self._verify_ssl = device_info.get(CONF_VERIFY_SSL)
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
# DigestAuth is not supported # DigestAuth is not supported
if ( if (
@ -130,11 +134,17 @@ class MjpegCamera(Camera):
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
_LOGGER.error("Error getting new camera image from %s: %s", self._name, err) _LOGGER.error("Error getting new camera image from %s: %s", self._name, err)
def camera_image(self): return None
def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
if self._username and self._password: if self._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION: if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password) auth: HTTPDigestAuth | HTTPBasicAuth = HTTPDigestAuth(
self._username, self._password
)
else: else:
auth = HTTPBasicAuth(self._username, self._password) auth = HTTPBasicAuth(self._username, self._password)
req = requests.get( req = requests.get(

View file

@ -1,4 +1,6 @@
"""Camera that loads a picture from an MQTT topic.""" """Camera that loads a picture from an MQTT topic."""
from __future__ import annotations
import functools import functools
import voluptuous as vol import voluptuous as vol
@ -98,6 +100,8 @@ class MqttCamera(MqttEntity, Camera):
}, },
) )
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response.""" """Return image response."""
return self._last_image return self._last_image

View file

@ -66,7 +66,9 @@ class NeatoCleaningMap(Camera):
self._image_url: str | None = None self._image_url: str | None = None
self._image: bytes | None = None self._image: bytes | None = None
def camera_image(self) -> bytes | None: def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response.""" """Return image response."""
self.update() self.update()
return self._image return self._image

View file

@ -180,7 +180,9 @@ class NestCamera(Camera):
self._device.add_update_listener(self.async_write_ha_state) self._device.add_update_listener(self.async_write_ha_state)
) )
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
# Returns the snapshot of the last event for ~30 seconds after the event # Returns the snapshot of the last event for ~30 seconds after the event
active_event_image = await self._async_active_event_image() active_event_image = await self._async_active_event_image()

View file

@ -1,4 +1,6 @@
"""Support for Nest Cameras.""" """Support for Nest Cameras."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -131,7 +133,9 @@ class NestCamera(Camera):
def _ready_for_snapshot(self, now): def _ready_for_snapshot(self, now):
return self._next_snapshot_at is None or now > self._next_snapshot_at return self._next_snapshot_at is None or now > self._next_snapshot_at
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
now = utcnow() now = utcnow()
if self._ready_for_snapshot(now): if self._ready_for_snapshot(now):

View file

@ -194,10 +194,14 @@ class NetatmoCamera(NetatmoBase, Camera):
self.data_handler.data[self._data_classes[0]["name"]], self.data_handler.data[self._data_classes[0]["name"]],
) )
async def async_camera_image(self) -> bytes | None: async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
try: try:
return await self._data.async_get_live_snapshot(camera_id=self._id) return cast(
bytes, await self._data.async_get_live_snapshot(camera_id=self._id)
)
except ( except (
aiohttp.ClientPayloadError, aiohttp.ClientPayloadError,
aiohttp.ContentTypeError, aiohttp.ContentTypeError,

View file

@ -1,4 +1,6 @@
"""Support for ONVIF Cameras with FFmpeg as decoder.""" """Support for ONVIF Cameras with FFmpeg as decoder."""
from __future__ import annotations
import asyncio import asyncio
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
@ -120,7 +122,9 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
"""Return the stream source.""" """Return the stream source."""
return self._stream_uri return self._stream_uri
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
image = None image = None

View file

@ -1,4 +1,6 @@
"""Proxy camera platform that enables image processing of camera data.""" """Proxy camera platform that enables image processing of camera data."""
from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import io import io
@ -219,13 +221,17 @@ class ProxyCamera(Camera):
self._last_image = None self._last_image = None
self._mode = config.get(CONF_MODE) self._mode = config.get(CONF_MODE)
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return camera image.""" """Return camera image."""
return asyncio.run_coroutine_threadsafe( return asyncio.run_coroutine_threadsafe(
self.async_camera_image(), self.hass.loop self.async_camera_image(), self.hass.loop
).result() ).result()
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
now = dt_util.utcnow() now = dt_util.utcnow()
@ -244,13 +250,13 @@ class ProxyCamera(Camera):
job = _resize_image job = _resize_image
else: else:
job = _crop_image job = _crop_image
image = await self.hass.async_add_executor_job( image_bytes: bytes = await self.hass.async_add_executor_job(
job, image.content, self._image_opts job, image.content, self._image_opts
) )
if self._cache_images: if self._cache_images:
self._last_image = image self._last_image = image_bytes
return image return image_bytes
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images.""" """Generate an HTTP MJPEG stream from camera images."""

View file

@ -1,4 +1,6 @@
"""Camera platform that receives images through HTTP POST.""" """Camera platform that receives images through HTTP POST."""
from __future__ import annotations
import asyncio import asyncio
from collections import deque from collections import deque
from datetime import timedelta from datetime import timedelta
@ -155,7 +157,9 @@ class PushCamera(Camera):
self.async_write_ha_state() self.async_write_ha_state()
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response.""" """Return a still image response."""
if self.queue: if self.queue:
if self._state == STATE_IDLE: if self._state == STATE_IDLE:

View file

@ -1,4 +1,5 @@
"""Support for QVR Pro streams.""" """Support for QVR Pro streams."""
from __future__ import annotations
import logging import logging
@ -88,7 +89,9 @@ class QVRProCamera(Camera):
return attrs return attrs
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Get image bytes from camera.""" """Get image bytes from camera."""
try: try:
return self._client.get_snapshot(self.guid) return self._client.get_snapshot(self.guid)

View file

@ -1,4 +1,6 @@
"""This component provides support to the Ring Door Bell camera.""" """This component provides support to the Ring Door Bell camera."""
from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from itertools import chain from itertools import chain
@ -101,7 +103,9 @@ class RingCam(RingEntityMixin, Camera):
"last_video_id": self._last_video_id, "last_video_id": self._last_video_id,
} }
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
ffmpeg = ImageFrame(self._ffmpeg.binary) ffmpeg = ImageFrame(self._ffmpeg.binary)

View file

@ -1,4 +1,6 @@
"""Camera platform that has a Raspberry Pi camera.""" """Camera platform that has a Raspberry Pi camera."""
from __future__ import annotations
import logging import logging
import os import os
import shutil import shutil
@ -122,7 +124,9 @@ class RaspberryCamera(Camera):
): ):
pass pass
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return raspistill image response.""" """Return raspistill image response."""
with open(self._config[CONF_FILE_PATH], "rb") as file: with open(self._config[CONF_FILE_PATH], "rb") as file:
return file.read() return file.read()

View file

@ -1,4 +1,6 @@
"""Camera support for the Skybell HD Doorbell.""" """Camera support for the Skybell HD Doorbell."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -75,7 +77,9 @@ class SkybellCamera(SkybellDevice, Camera):
return self._device.activity_image return self._device.activity_image
return self._device.image return self._device.image
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Get the latest camera image.""" """Get the latest camera image."""
super().update() super().update()

View file

@ -123,7 +123,9 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera):
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return]
def camera_image(self) -> bytes | None: def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
_LOGGER.debug( _LOGGER.debug(
"SynoDSMCamera.camera_image(%s)", "SynoDSMCamera.camera_image(%s)",

View file

@ -194,10 +194,12 @@ class UnifiVideoCamera(Camera):
self._caminfo = caminfo self._caminfo = caminfo
return True return True
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return the image of this camera.""" """Return the image of this camera."""
if not self._camera and not self._login(): if not self._camera and not self._login():
return return None
def _get_image(retry=True): def _get_image(retry=True):
try: try:

View file

@ -79,7 +79,9 @@ class VerisureSmartcam(CoordinatorEntity, Camera):
"via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]),
} }
def camera_image(self) -> bytes | None: def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return image response.""" """Return image response."""
self.check_imagelist() self.check_imagelist()
if not self._image: if not self._image:

View file

@ -1,4 +1,6 @@
"""Support for Vivotek IP Cameras.""" """Support for Vivotek IP Cameras."""
from __future__ import annotations
from libpyvivotek import VivotekCamera from libpyvivotek import VivotekCamera
import voluptuous as vol import voluptuous as vol
@ -87,7 +89,9 @@ class VivotekCam(Camera):
"""Return the interval between frames of the mjpeg stream.""" """Return the interval between frames of the mjpeg stream."""
return self._frame_interval return self._frame_interval
def camera_image(self): def camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image.""" """Return bytes of camera image."""
return self._cam.snapshot() return self._cam.snapshot()

View file

@ -1,4 +1,6 @@
"""Support for Xeoma Cameras.""" """Support for Xeoma Cameras."""
from __future__ import annotations
import logging import logging
from pyxeoma.xeoma import Xeoma, XeomaError from pyxeoma.xeoma import Xeoma, XeomaError
@ -109,7 +111,9 @@ class XeomaCamera(Camera):
self._password = password self._password = password
self._last_image = None self._last_image = None
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
try: try:

View file

@ -1,4 +1,6 @@
"""This component provides support for Xiaomi Cameras.""" """This component provides support for Xiaomi Cameras."""
from __future__ import annotations
import asyncio import asyncio
from ftplib import FTP, error_perm from ftplib import FTP, error_perm
import logging import logging
@ -138,7 +140,9 @@ class XiaomiCamera(Camera):
return f"ftp://{self.user}:{self.passwd}@{host}:{self.port}{ftp.pwd()}/{video}" return f"ftp://{self.user}:{self.passwd}@{host}:{self.port}{ftp.pwd()}/{video}"
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
try: try:

View file

@ -1,4 +1,6 @@
"""Support for Xiaomi Cameras (HiSilicon Hi3518e V200).""" """Support for Xiaomi Cameras (HiSilicon Hi3518e V200)."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
@ -119,7 +121,9 @@ class YiCamera(Camera):
self._is_on = False self._is_on = False
return None return None
async def async_camera_image(self): async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
url = await self._get_latest_video_url() url = await self._get_latest_video_url()
if url and url != self._last_url: if url and url != self._last_url:

View file

@ -57,7 +57,7 @@ PySocks==1.7.1
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
# homeassistant.components.homekit # homeassistant.components.camera
PyTurboJPEG==1.5.0 PyTurboJPEG==1.5.0
# homeassistant.components.vicare # homeassistant.components.vicare

View file

@ -26,7 +26,7 @@ PyRMVtransport==0.3.2
# homeassistant.components.transport_nsw # homeassistant.components.transport_nsw
PyTransportNSW==0.1.1 PyTransportNSW==0.1.1
# homeassistant.components.homekit # homeassistant.components.camera
PyTurboJPEG==1.5.0 PyTurboJPEG==1.5.0
# homeassistant.components.xiaomi_aqara # homeassistant.components.xiaomi_aqara

View file

@ -3,8 +3,12 @@
All containing methods are legacy helpers that should not be used by new All containing methods are legacy helpers that should not be used by new
components. Instead call the service directly. components. Instead call the service directly.
""" """
from unittest.mock import Mock
from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM from homeassistant.components.camera.const import DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM
EMPTY_8_6_JPEG = b"empty_8_6"
def mock_camera_prefs(hass, entity_id, prefs=None): def mock_camera_prefs(hass, entity_id, prefs=None):
"""Fixture for cloud component.""" """Fixture for cloud component."""
@ -13,3 +17,16 @@ def mock_camera_prefs(hass, entity_id, prefs=None):
prefs_to_set.update(prefs) prefs_to_set.update(prefs)
hass.data[DATA_CAMERA_PREFS]._prefs[entity_id] = prefs_to_set hass.data[DATA_CAMERA_PREFS]._prefs[entity_id] = prefs_to_set
return prefs_to_set return prefs_to_set
def mock_turbo_jpeg(
first_width=None, second_width=None, first_height=None, second_height=None
):
"""Mock a TurboJPEG instance."""
mocked_turbo_jpeg = Mock()
mocked_turbo_jpeg.decode_header.side_effect = [
(first_width, first_height, 0, 0),
(second_width, second_height, 0, 0),
]
mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG
return mocked_turbo_jpeg

View file

@ -1,8 +1,10 @@
"""Test HomeKit img_util module.""" """Test img_util module."""
from unittest.mock import patch from unittest.mock import patch
from turbojpeg import TurboJPEG
from homeassistant.components.camera import Image from homeassistant.components.camera import Image
from homeassistant.components.homekit.img_util import ( from homeassistant.components.camera.img_util import (
TurboJPEGSingleton, TurboJPEGSingleton,
scale_jpeg_camera_image, scale_jpeg_camera_image,
) )
@ -12,13 +14,23 @@ from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg
EMPTY_16_12_JPEG = b"empty_16_12" EMPTY_16_12_JPEG = b"empty_16_12"
def _clear_turbojpeg_singleton():
TurboJPEGSingleton.__instance = None
def _reset_turbojpeg_singleton():
TurboJPEGSingleton.__instance = TurboJPEG()
def test_turbojpeg_singleton(): def test_turbojpeg_singleton():
"""Verify the instance always gives back the same.""" """Verify the instance always gives back the same."""
_clear_turbojpeg_singleton()
assert TurboJPEGSingleton.instance() == TurboJPEGSingleton.instance() assert TurboJPEGSingleton.instance() == TurboJPEGSingleton.instance()
def test_scale_jpeg_camera_image(): def test_scale_jpeg_camera_image():
"""Test we can scale a jpeg image.""" """Test we can scale a jpeg image."""
_clear_turbojpeg_singleton()
camera_image = Image("image/jpeg", EMPTY_16_12_JPEG) camera_image = Image("image/jpeg", EMPTY_16_12_JPEG)
@ -27,6 +39,12 @@ def test_scale_jpeg_camera_image():
TurboJPEGSingleton() TurboJPEGSingleton()
assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content
turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12)
turbo_jpeg.decode_header.side_effect = OSError
with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg):
TurboJPEGSingleton()
assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content
turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12)
with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg):
TurboJPEGSingleton() TurboJPEGSingleton()
@ -44,11 +62,11 @@ def test_scale_jpeg_camera_image():
def test_turbojpeg_load_failure(): def test_turbojpeg_load_failure():
"""Handle libjpegturbo not being installed.""" """Handle libjpegturbo not being installed."""
_clear_turbojpeg_singleton()
with patch("turbojpeg.TurboJPEG", side_effect=Exception): with patch("turbojpeg.TurboJPEG", side_effect=Exception):
TurboJPEGSingleton() TurboJPEGSingleton()
assert TurboJPEGSingleton.instance() is False assert TurboJPEGSingleton.instance() is False
with patch("turbojpeg.TurboJPEG"): _clear_turbojpeg_singleton()
TurboJPEGSingleton() TurboJPEGSingleton()
assert TurboJPEGSingleton.instance() assert TurboJPEGSingleton.instance() is not None

View file

@ -20,6 +20,8 @@ from homeassistant.const import (
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg
from tests.components.camera import common from tests.components.camera import common
@ -75,6 +77,51 @@ async def test_get_image_from_camera(hass, image_mock_url):
assert image.content == b"Test" assert image.content == b"Test"
async def test_get_image_from_camera_with_width_height(hass, image_mock_url):
"""Grab an image from camera entity with width and height."""
turbo_jpeg = mock_turbo_jpeg(
first_width=16, first_height=12, second_width=300, second_height=200
)
with patch(
"homeassistant.components.camera.img_util.TurboJPEGSingleton.instance",
return_value=turbo_jpeg,
), patch(
"homeassistant.components.demo.camera.Path.read_bytes",
autospec=True,
return_value=b"Test",
) as mock_camera:
image = await camera.async_get_image(
hass, "camera.demo_camera", width=640, height=480
)
assert mock_camera.called
assert image.content == b"Test"
async def test_get_image_from_camera_with_width_height_scaled(hass, image_mock_url):
"""Grab an image from camera entity with width and height and scale it."""
turbo_jpeg = mock_turbo_jpeg(
first_width=16, first_height=12, second_width=300, second_height=200
)
with patch(
"homeassistant.components.camera.img_util.TurboJPEGSingleton.instance",
return_value=turbo_jpeg,
), patch(
"homeassistant.components.demo.camera.Path.read_bytes",
autospec=True,
return_value=b"Valid jpeg",
) as mock_camera:
image = await camera.async_get_image(
hass, "camera.demo_camera", width=4, height=3
)
assert mock_camera.called
assert image.content_type == "image/jpeg"
assert image.content == EMPTY_8_6_JPEG
async def test_get_stream_source_from_camera(hass, mock_camera): async def test_get_stream_source_from_camera(hass, mock_camera):
"""Fetch stream source from camera entity.""" """Fetch stream source from camera entity."""

View file

@ -1,17 +0,0 @@
"""Collection of fixtures and functions for the HomeKit tests."""
from unittest.mock import Mock
EMPTY_8_6_JPEG = b"empty_8_6"
def mock_turbo_jpeg(
first_width=None, second_width=None, first_height=None, second_height=None
):
"""Mock a TurboJPEG instance."""
mocked_turbo_jpeg = Mock()
mocked_turbo_jpeg.decode_header.side_effect = [
(first_width, first_height, 0, 0),
(second_width, second_height, 0, 0),
]
mocked_turbo_jpeg.scale_with_quality.return_value = EMPTY_8_6_JPEG
return mocked_turbo_jpeg

View file

@ -7,6 +7,7 @@ from pyhap.accessory_driver import AccessoryDriver
import pytest import pytest
from homeassistant.components import camera, ffmpeg from homeassistant.components import camera, ffmpeg
from homeassistant.components.camera.img_util import TurboJPEGSingleton
from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
AUDIO_CODEC_COPY, AUDIO_CODEC_COPY,
@ -26,14 +27,13 @@ from homeassistant.components.homekit.const import (
VIDEO_CODEC_COPY, VIDEO_CODEC_COPY,
VIDEO_CODEC_H264_OMX, VIDEO_CODEC_H264_OMX,
) )
from homeassistant.components.homekit.img_util import TurboJPEGSingleton
from homeassistant.components.homekit.type_cameras import Camera from homeassistant.components.homekit.type_cameras import Camera
from homeassistant.components.homekit.type_switches import Switch from homeassistant.components.homekit.type_switches import Switch
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .common import mock_turbo_jpeg from tests.components.camera.common import mock_turbo_jpeg
MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA"
MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA=="