Pass width and height when requesting camera snapshot (#53835)
This commit is contained in:
parent
390023a576
commit
e99576c094
53 changed files with 418 additions and 113 deletions
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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(
|
||||||
cam_image.content,
|
bytes,
|
||||||
scaling_factor=scaling_factor,
|
turbo_jpeg.scale_with_quality(
|
||||||
quality=75,
|
cam_image.content,
|
||||||
|
scaling_factor=scaling_factor,
|
||||||
|
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
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)",
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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."""
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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=="
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue