mirror of
https://github.com/home-assistant/core.git
synced 2026-09-10 15:41:22 +01:00
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""Last motion and last ring image entities for a DoorBird device."""
|
|
|
|
# These replace the same-named camera entities, which exposed stills through the
|
|
# camera UI even though no live video is involved. The legacy camera entities are
|
|
# kept to avoid breaking existing dashboards and automations; a follow-up should
|
|
# deprecate them via a repair issue once users have had time to migrate.
|
|
|
|
from dataclasses import dataclass
|
|
from typing import override
|
|
|
|
import aiohttp
|
|
|
|
from homeassistant.components.image import (
|
|
Image,
|
|
ImageEntity,
|
|
ImageEntityDescription,
|
|
infer_image_type,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from .const import DOMAIN
|
|
from .entity import DoorBirdEntity
|
|
from .models import DoorBirdConfigEntry, DoorBirdData
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class DoorBirdImageEntityDescription(ImageEntityDescription):
|
|
"""Describes a DoorBird image entity."""
|
|
|
|
doorbird_event_type: str
|
|
|
|
|
|
IMAGE_DESCRIPTIONS: tuple[DoorBirdImageEntityDescription, ...] = (
|
|
DoorBirdImageEntityDescription(
|
|
key="last_motion",
|
|
translation_key="last_motion",
|
|
doorbird_event_type="motion",
|
|
),
|
|
DoorBirdImageEntityDescription(
|
|
key="last_ring",
|
|
translation_key="last_ring",
|
|
doorbird_event_type="doorbell",
|
|
),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: DoorBirdConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the DoorBird image platform."""
|
|
door_bird_data = config_entry.runtime_data
|
|
async_add_entities(
|
|
DoorBirdLastEventImage(hass, door_bird_data, description)
|
|
for description in IMAGE_DESCRIPTIONS
|
|
)
|
|
|
|
|
|
class DoorBirdLastEventImage(ImageEntity, DoorBirdEntity):
|
|
"""An image of the last motion or last ring on a DoorBird device."""
|
|
|
|
entity_description: DoorBirdImageEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
hass: HomeAssistant,
|
|
door_bird_data: DoorBirdData,
|
|
description: DoorBirdImageEntityDescription,
|
|
) -> None:
|
|
"""Initialize the image entity."""
|
|
ImageEntity.__init__(self, hass)
|
|
DoorBirdEntity.__init__(self, door_bird_data)
|
|
self.entity_description = description
|
|
self._attr_unique_id = f"{self._mac_addr}_{description.key}"
|
|
history_type = (
|
|
"doorbell"
|
|
if description.doorbird_event_type == "doorbell"
|
|
else "motionsensor"
|
|
)
|
|
self._image_url = self._door_station.device.history_image_url(1, history_type)
|
|
self._matching_event_names = [
|
|
event.event
|
|
for event in self._door_station.event_descriptions
|
|
if event.event_type == description.doorbird_event_type
|
|
]
|
|
|
|
@override
|
|
async def async_image(self) -> bytes | None:
|
|
"""Return bytes of the last event image."""
|
|
if self._cached_image:
|
|
return self._cached_image.content
|
|
try:
|
|
# No explicit timeout here — the image framework wraps async_image() in its
|
|
# own asyncio.timeout(IMAGE_TIMEOUT) and raises HTTP 500 on expiry.
|
|
image_bytes = await self._door_station.device.get_image(self._image_url)
|
|
except aiohttp.ClientError as error:
|
|
raise HomeAssistantError(
|
|
f"Error getting image from DoorBird: {error}"
|
|
) from error
|
|
content_type = infer_image_type(image_bytes)
|
|
if content_type is None:
|
|
raise HomeAssistantError("DoorBird returned an unrecognized image")
|
|
self._cached_image = Image(content_type=content_type, content=image_bytes)
|
|
self._attr_content_type = content_type
|
|
return image_bytes
|
|
|
|
@override
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Subscribe to the underlying DoorBird events."""
|
|
await super().async_added_to_hass()
|
|
for event_name in self._matching_event_names:
|
|
self.async_on_remove(
|
|
async_dispatcher_connect(
|
|
self.hass,
|
|
f"{DOMAIN}_{event_name}",
|
|
self._async_handle_event,
|
|
)
|
|
)
|
|
|
|
@callback
|
|
def _async_handle_event(self) -> None:
|
|
"""Bust the cache and bump the last-updated timestamp on a new event."""
|
|
self._cached_image = None
|
|
self._attr_image_last_updated = dt_util.utcnow()
|
|
self.async_write_ha_state()
|