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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 mC_k&e;!^T!U#yD2|KB3sag_F5=9bJ) literal 0 HcmV?d00001