From 6c426fea9e5718efc43d7bcd0ea8335d47fec222 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 30 Oct 2021 13:44:28 -0700 Subject: [PATCH] Serve nest placeholder image from disk rather than generate on the fly (#58663) * Serve placeholder image from disk rather than generate on the flay The placeholder image was generated from hoome assistant, saved, flipped, and crushed a bit. The image is 640x480 and the integration does not support any on the fly resizing. * Cache Nest WebRTC placeholder image on camera Cache Nest WebRTC placeholder image rather than reading from disk every time. --- homeassistant/components/nest/camera_sdm.py | 48 ++++-------------- homeassistant/components/nest/placeholder.png | Bin 0 -> 2689 bytes 2 files changed, 9 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/nest/placeholder.png diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index abebc8db3ef..71798eb40c3 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -3,11 +3,10 @@ from __future__ import annotations from collections.abc import Callable import datetime -import io import logging +from pathlib import Path from typing import Any -from PIL import Image, ImageDraw, ImageFilter from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, @@ -37,18 +36,11 @@ from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) +PLACEHOLDER = Path(__file__).parent / "placeholder.png" + # Used to schedule an alarm to refresh the stream before expiration STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) -# The Google Home app dispays a placeholder image that appears as a faint -# light source (dim, blurred sphere) giving the user an indication the camera -# is available, not just a blank screen. These constants define a blurred -# ellipse at the top left of the thumbnail. -PLACEHOLDER_ELLIPSE_BLUR = 0.1 -PLACEHOLDER_ELLIPSE_XY = [-0.4, 0.3, 0.3, 0.4] -PLACEHOLDER_OVERLAY_COLOR = "#ffffff" -PLACEHOLDER_ELLIPSE_OPACITY = 255 - async def async_setup_sdm_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -73,30 +65,6 @@ async def async_setup_sdm_entry( async_add_entities(entities) -def placeholder_image(width: int | None = None, height: int | None = None) -> Image: - """Return a camera image preview for cameras without live thumbnails.""" - if not width or not height: - return Image.new("RGB", (1, 1)) - # Draw a dark scene with a fake light source - blank = Image.new("RGB", (width, height)) - overlay = Image.new("RGB", blank.size, color=PLACEHOLDER_OVERLAY_COLOR) - ellipse = Image.new("L", blank.size, color=0) - draw = ImageDraw.Draw(ellipse) - draw.ellipse( - ( - width * PLACEHOLDER_ELLIPSE_XY[0], - height * PLACEHOLDER_ELLIPSE_XY[1], - width * PLACEHOLDER_ELLIPSE_XY[2], - height * PLACEHOLDER_ELLIPSE_XY[3], - ), - fill=PLACEHOLDER_ELLIPSE_OPACITY, - ) - mask = ellipse.filter( - ImageFilter.GaussianBlur(radius=width * PLACEHOLDER_ELLIPSE_BLUR) - ) - return Image.composite(overlay, blank, mask) - - class NestCamera(Camera): """Devices that support cameras.""" @@ -112,6 +80,7 @@ class NestCamera(Camera): self._event_image_bytes: bytes | None = None self._event_image_cleanup_unsub: Callable[[], None] | None = None self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self._placeholder_image: bytes | None = None @property def should_poll(self) -> bool: @@ -251,10 +220,11 @@ class NestCamera(Camera): return None # Nest Web RTC cams only have image previews for events, and not # for "now" by design to save batter, and need a placeholder. - image = placeholder_image(width=width, height=height) - with io.BytesIO() as content: - image.save(content, format="JPEG", optimize=True) - return content.getvalue() + if not self._placeholder_image: + self._placeholder_image = await self.hass.async_add_executor_job( + PLACEHOLDER.read_bytes + ) + return self._placeholder_image return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) async def _async_active_event_image(self) -> bytes | None: diff --git a/homeassistant/components/nest/placeholder.png b/homeassistant/components/nest/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..5ccc755abfd3ccda6c43b08cfc8352717a3208c2 GIT binary patch literal 2689 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 literal 0 HcmV?d00001