diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 51f565a0980..39422c530b3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -32,12 +32,10 @@ if TYPE_CHECKING: ServiceCall, UserService DOMAIN = 'esphome' -REQUIREMENTS = ['aioesphomeapi==1.6.0'] +REQUIREMENTS = ['aioesphomeapi==1.7.0'] _LOGGER = logging.getLogger(__name__) -DOMAIN = 'esphome' - DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}' DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}' DISPATCHER_ON_LIST = 'esphome_{entry_id}_on_list' @@ -50,6 +48,7 @@ STORAGE_VERSION = 1 # The HA component types this integration supports HA_COMPONENTS = [ 'binary_sensor', + 'camera', 'cover', 'fan', 'light', @@ -543,7 +542,7 @@ class EsphomeEntity(Entity): self._remove_callbacks.append( async_dispatcher_connect(self.hass, DISPATCHER_UPDATE_ENTITY.format(**kwargs), - self.async_schedule_update_ha_state) + self._on_update) ) self._remove_callbacks.append( @@ -558,6 +557,10 @@ class EsphomeEntity(Entity): self.async_schedule_update_ha_state) ) + async def _on_update(self): + """Update the entity state when state or static info changed.""" + self.async_schedule_update_ha_state() + async def async_will_remove_from_hass(self): """Unregister callbacks.""" for remove_callback in self._remove_callbacks: diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py new file mode 100644 index 00000000000..319a2c2a4d9 --- /dev/null +++ b/homeassistant/components/esphome/camera.py @@ -0,0 +1,83 @@ +"""Support for ESPHome cameras.""" +import asyncio +import logging +from typing import Optional, TYPE_CHECKING + +from homeassistant.components import camera +from homeassistant.components.camera import Camera +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType +from . import EsphomeEntity, platform_async_setup_entry + +if TYPE_CHECKING: + # pylint: disable=unused-import + from aioesphomeapi import CameraInfo, CameraState # noqa + +DEPENDENCIES = ['esphome'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistantType, + entry: ConfigEntry, async_add_entities) -> None: + """Set up esphome cameras based on a config entry.""" + # pylint: disable=redefined-outer-name + from aioesphomeapi import CameraInfo, CameraState # noqa + + await platform_async_setup_entry( + hass, entry, async_add_entities, + component_key='camera', + info_type=CameraInfo, entity_type=EsphomeCamera, + state_type=CameraState + ) + + +class EsphomeCamera(Camera, EsphomeEntity): + """A camera implementation for ESPHome.""" + + def __init__(self, entry_id: str, component_key: str, key: int): + """Initialize.""" + Camera.__init__(self) + EsphomeEntity.__init__(self, entry_id, component_key, key) + self._image_cond = asyncio.Condition() + + @property + def _static_info(self) -> 'CameraInfo': + return super()._static_info + + @property + def _state(self) -> Optional['CameraState']: + return super()._state + + async def _on_update(self): + """Notify listeners of new image when update arrives.""" + await super()._on_update() + async with self._image_cond: + self._image_cond.notify_all() + + async def async_camera_image(self) -> Optional[bytes]: + """Return single camera image bytes.""" + if not self.available: + return None + await self._client.request_single_image() + async with self._image_cond: + await self._image_cond.wait() + if not self.available: + return None + return self._state.image[:] + + async def _async_camera_stream_image(self) -> Optional[bytes]: + """Return a single camera image in a stream.""" + if not self.available: + return None + await self._client.request_image_stream() + async with self._image_cond: + await self._image_cond.wait() + if not self.available: + return None + return self._state.image[:] + + async def handle_async_mjpeg_stream(self, request): + """Serve an HTTP MJPEG stream from the camera.""" + return await camera.async_get_still_stream( + request, self._async_camera_stream_image, + camera.DEFAULT_CONTENT_TYPE, 0.0) diff --git a/requirements_all.txt b/requirements_all.txt index 622ea15f8fb..512079a2347 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -109,7 +109,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.6.0 +aioesphomeapi==1.7.0 # homeassistant.components.freebox aiofreepybox==0.0.6