"""Support for Xiaomi Cameras (HiSilicon Hi3518e V200).""" import logging from typing import override from aioftp import Client, StatusCodeError from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import ( PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA, Camera, ) from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PATH, CONF_PORT, CONF_USERNAME, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) DEFAULT_BRAND = "YI Home Camera" DEFAULT_PASSWORD = "" DEFAULT_PATH = "/tmp/sd/record" # noqa: S108 DEFAULT_PORT = 21 DEFAULT_USERNAME = "root" DEFAULT_ARGUMENTS = "-pred 1" CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, } ) async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a Yi Camera.""" async_add_entities([YiCamera(hass, config)], True) class YiCamera(Camera): """Define an implementation of a Yi Camera.""" _attr_brand = DEFAULT_BRAND def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) self._last_image: bytes | None = None self._last_url = None self._manager = get_ffmpeg_manager(hass) self._attr_name = config[CONF_NAME] self.host = config[CONF_HOST] self.port = config[CONF_PORT] self.path = config[CONF_PATH] self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" ftp = Client() try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) except (ConnectionRefusedError, StatusCodeError) as err: raise PlatformNotReady(err) from err try: await ftp.change_directory(self.path) dirs = [] for path, attrs in await ftp.list(): if attrs["type"] == "dir" and "." not in str(path): dirs.append(path) latest_dir = dirs[-1] await ftp.change_directory(latest_dir) videos = [] for path, _ in await ftp.list(): videos.append(path) if not videos: _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) return None await ftp.quit() self._attr_is_on = True return ( f"ftp://{self.user}:{self.passwd}@{self.host}:" f"{self.port}{self.path}/{latest_dir}/{videos[-1]}" ) except (ConnectionRefusedError, StatusCodeError) as err: _LOGGER.error("Error while fetching video: %s", err) self._attr_is_on = False return None @override async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" url = await self._get_latest_video_url() if url and url != self._last_url: self._last_image = await ffmpeg.async_get_image( self.hass, url, extra_cmd=self._extra_arguments, width=width, height=height, ) self._last_url = url return self._last_image @override async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" if not self._attr_is_on: return None stream = CameraMjpeg(self._manager.binary) await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( self.hass, request, stream_reader, self._manager.ffmpeg_stream_content_type, ) finally: await stream.close()