Add an image placeholder for Nest WebRTC cameras (#58250)

This commit is contained in:
Allen Porter 2021-10-28 21:07:29 -07:00 committed by GitHub
parent 6d30105c9f
commit 9c5a79c641
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 52 additions and 6 deletions

View file

@ -3,9 +3,11 @@ 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 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,
@ -38,6 +40,15 @@ _LOGGER = logging.getLogger(__name__)
# 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
@ -62,6 +73,30 @@ 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."""
@ -212,7 +247,14 @@ class NestCamera(Camera):
# Fetch still image from the live stream # Fetch still image from the live stream
stream_url = await self.stream_source() stream_url = await self.stream_source()
if not stream_url: if not stream_url:
if self.frontend_stream_type != STREAM_TYPE_WEB_RTC:
return None 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()
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:

View file

@ -135,7 +135,7 @@ async def fire_alarm(hass, point_in_time):
await hass.async_block_till_done() await hass.async_block_till_done()
async def async_get_image(hass): async def async_get_image(hass, width=None, height=None):
"""Get image from the camera, a wrapper around camera.async_get_image.""" """Get image from the camera, a wrapper around camera.async_get_image."""
# Note: this patches ImageFrame to simulate decoding an image from a live # Note: this patches ImageFrame to simulate decoding an image from a live
# stream, however the test may not use it. Tests assert on the image # stream, however the test may not use it. Tests assert on the image
@ -145,7 +145,9 @@ async def async_get_image(hass):
autopatch=True, autopatch=True,
return_value=IMAGE_BYTES_FROM_STREAM, return_value=IMAGE_BYTES_FROM_STREAM,
): ):
return await camera.async_get_image(hass, "camera.my_camera") return await camera.async_get_image(
hass, "camera.my_camera", width=width, height=height
)
async def test_no_devices(hass): async def test_no_devices(hass):
@ -721,9 +723,11 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client):
assert msg["success"] assert msg["success"]
assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" assert msg["result"]["answer"] == "v=0\r\ns=-\r\n"
# Nest WebRTC cameras do not support a still image # Nest WebRTC cameras return a placeholder
with pytest.raises(HomeAssistantError): content = await async_get_image(hass)
await async_get_image(hass) assert content.content_type == "image/jpeg"
content = await async_get_image(hass, width=1024, height=768)
assert content.content_type == "image/jpeg"
async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client):