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.
pull/58773/head
Allen Porter 2021-10-30 13:44:28 -07:00 committed by GitHub
parent 972dbac1f7
commit 6c426fea9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 9 additions and 39 deletions

View File

@ -3,11 +3,10 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
import datetime import datetime
import io
import logging import logging
from pathlib import Path
from typing import Any from typing import Any
from PIL import Image, ImageDraw, ImageFilter
from google_nest_sdm.camera_traits import ( from google_nest_sdm.camera_traits import (
CameraEventImageTrait, CameraEventImageTrait,
CameraImageTrait, CameraImageTrait,
@ -37,18 +36,11 @@ from .device_info import NestDeviceInfo
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLACEHOLDER = Path(__file__).parent / "placeholder.png"
# Used to schedule an alarm to refresh the stream before expiration # Used to schedule an alarm to refresh the stream before expiration
STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) 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( async def async_setup_sdm_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -73,30 +65,6 @@ async def async_setup_sdm_entry(
async_add_entities(entities) 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): class NestCamera(Camera):
"""Devices that support cameras.""" """Devices that support cameras."""
@ -112,6 +80,7 @@ class NestCamera(Camera):
self._event_image_bytes: bytes | None = None self._event_image_bytes: bytes | None = None
self._event_image_cleanup_unsub: Callable[[], None] | None = None self._event_image_cleanup_unsub: Callable[[], None] | None = None
self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits
self._placeholder_image: bytes | None = None
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
@ -251,10 +220,11 @@ class NestCamera(Camera):
return None return None
# Nest Web RTC cams only have image previews for events, and not # Nest Web RTC cams only have image previews for events, and not
# for "now" by design to save batter, and need a placeholder. # for "now" by design to save batter, and need a placeholder.
image = placeholder_image(width=width, height=height) if not self._placeholder_image:
with io.BytesIO() as content: self._placeholder_image = await self.hass.async_add_executor_job(
image.save(content, format="JPEG", optimize=True) PLACEHOLDER.read_bytes
return content.getvalue() )
return self._placeholder_image
return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG)
async def _async_active_event_image(self) -> bytes | None: async def _async_active_event_image(self) -> bytes | None:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB