1 """Proxy camera platform that enables image processing of camera data."""
3 from __future__
import annotations
6 from datetime
import timedelta
11 import voluptuous
as vol
14 PLATFORM_SCHEMA
as CAMERA_PLATFORM_SCHEMA,
17 async_get_mjpeg_stream,
18 async_get_still_stream,
28 _LOGGER = logging.getLogger(__name__)
30 CONF_CACHE_IMAGES =
"cache_images"
31 CONF_FORCE_RESIZE =
"force_resize"
32 CONF_IMAGE_QUALITY =
"image_quality"
33 CONF_IMAGE_REFRESH_RATE =
"image_refresh_rate"
34 CONF_MAX_IMAGE_WIDTH =
"max_image_width"
35 CONF_MAX_IMAGE_HEIGHT =
"max_image_height"
36 CONF_MAX_STREAM_WIDTH =
"max_stream_width"
37 CONF_MAX_STREAM_HEIGHT =
"max_stream_height"
38 CONF_IMAGE_TOP =
"image_top"
39 CONF_IMAGE_LEFT =
"image_left"
40 CONF_STREAM_QUALITY =
"stream_quality"
42 MODE_RESIZE =
"resize"
45 DEFAULT_BASENAME =
"Camera Proxy"
48 PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
50 vol.Required(CONF_ENTITY_ID): cv.entity_id,
51 vol.Optional(CONF_NAME): cv.string,
52 vol.Optional(CONF_CACHE_IMAGES, default=
False): cv.boolean,
53 vol.Optional(CONF_FORCE_RESIZE, default=
False): cv.boolean,
54 vol.Optional(CONF_MODE, default=MODE_RESIZE): vol.In([MODE_RESIZE, MODE_CROP]),
55 vol.Optional(CONF_IMAGE_QUALITY): int,
56 vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
57 vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
58 vol.Optional(CONF_MAX_IMAGE_HEIGHT): int,
59 vol.Optional(CONF_MAX_STREAM_WIDTH): int,
60 vol.Optional(CONF_MAX_STREAM_HEIGHT): int,
61 vol.Optional(CONF_IMAGE_LEFT): int,
62 vol.Optional(CONF_IMAGE_TOP): int,
63 vol.Optional(CONF_STREAM_QUALITY): int,
71 async_add_entities: AddEntitiesCallback,
72 discovery_info: DiscoveryInfoType |
None =
None,
74 """Set up the Proxy camera platform."""
79 """Perform some pre-checks on the given image."""
83 img = Image.open(io.BytesIO(image))
84 except OSError
as err:
85 _LOGGER.warning(
"Failed to open image")
86 raise ValueError
from err
87 imgfmt =
str(img.format)
88 if imgfmt
not in (
"PNG",
"JPEG"):
89 _LOGGER.warning(
"Image is of unsupported type: %s", imgfmt)
92 img = img.convert(
"RGB")
103 quality = opts.quality
or DEFAULT_QUALITY
104 new_width = opts.max_width
105 (old_width, old_height) = img.size
106 old_size = len(image)
107 if old_width <= new_width:
108 if opts.quality
is None:
109 _LOGGER.debug(
"Image is smaller-than/equal-to requested width")
111 new_width = old_width
113 scale = new_width /
float(old_width)
116 img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
117 imgbuf = io.BytesIO()
118 img.save(imgbuf,
"JPEG", optimize=
True, quality=quality)
119 newimage = imgbuf.getvalue()
120 if not opts.force_resize
and len(newimage) >= old_size:
123 "Using original image (%d bytes) "
124 "because resized image (%d bytes) is not smaller"
132 "Resized image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
150 quality = opts.quality
or DEFAULT_QUALITY
151 (old_width, old_height) = img.size
152 old_size = len(image)
155 if opts.left
is None:
157 if opts.max_width
is None or opts.max_width > old_width - opts.left:
158 opts.max_width = old_width - opts.left
159 if opts.max_height
is None or opts.max_height > old_height - opts.top:
160 opts.max_height = old_height - opts.top
163 (opts.left, opts.top, opts.left + opts.max_width, opts.top + opts.max_height)
165 imgbuf = io.BytesIO()
166 img.save(imgbuf,
"JPEG", optimize=
True, quality=quality)
167 newimage = imgbuf.getvalue()
170 "Cropped image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
182 """The representation of image options."""
184 def __init__(self, max_width, max_height, left, top, quality, force_resize):
185 """Initialize image options."""
194 """Bool evaluation rules."""
199 """The representation of a Proxy camera."""
202 """Initialize a proxy camera component."""
207 config.get(CONF_NAME)
or f
"{DEFAULT_BASENAME} - {self._proxied_camera}"
210 config.get(CONF_MAX_IMAGE_WIDTH),
211 config.get(CONF_MAX_IMAGE_HEIGHT),
212 config.get(CONF_IMAGE_LEFT),
213 config.get(CONF_IMAGE_TOP),
214 config.get(CONF_IMAGE_QUALITY),
215 config.get(CONF_FORCE_RESIZE),
219 config.get(CONF_MAX_STREAM_WIDTH),
220 config.get(CONF_MAX_STREAM_HEIGHT),
221 config.get(CONF_IMAGE_LEFT),
222 config.get(CONF_IMAGE_TOP),
223 config.get(CONF_STREAM_QUALITY),
229 config.get(CONF_IMAGE_REFRESH_RATE)
or config.get(CONF_CACHE_IMAGES)
233 self.
_mode_mode = config.get(CONF_MODE)
236 self, width: int |
None =
None, height: int |
None =
None
238 """Return camera image."""
239 return asyncio.run_coroutine_threadsafe(
244 self, width: int |
None =
None, height: int |
None =
None
246 """Return a still image response from the camera."""
247 now = dt_util.utcnow()
257 _LOGGER.error(
"Error getting original camera image")
260 if self.
_mode_mode == MODE_RESIZE:
264 image_bytes: bytes = await self.
hasshasshass.async_add_executor_job(
273 """Generate an HTTP MJPEG stream from camera images."""
285 """Return the name of this camera."""
286 return self.
_name_name
289 """Return a still image response from the camera."""
294 except HomeAssistantError
as err:
295 raise asyncio.CancelledError
from err
297 if self.
_mode_mode == MODE_RESIZE:
301 return await self.
hasshasshass.async_add_executor_job(
float frame_interval(self)
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
def __init__(self, max_width, max_height, left, top, quality, force_resize)
def handle_async_mjpeg_stream(self, request)
def __init__(self, hass, config)
def _async_stream_image(self)
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
bytes|None camera_image(self, int|None width=None, int|None height=None)
Image async_get_image(HomeAssistant hass, str entity_id, int timeout=10, int|None width=None, int|None height=None)
web.StreamResponse|None async_get_mjpeg_stream(HomeAssistant hass, web.Request request, str entity_id)
web.StreamResponse async_get_still_stream(web.Request request, Callable[[], Awaitable[bytes|None]] image_cb, str content_type, float interval)
def _precheck_image(image, opts)
def _crop_image(image, opts)
def _resize_image(image, opts)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)