Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for Xiaomi Cameras (HiSilicon Hi3518e V200)."""
2 
3 from __future__ import annotations
4 
5 import logging
6 
7 from aioftp import Client, StatusCodeError
8 from haffmpeg.camera import CameraMjpeg
9 import voluptuous as vol
10 
11 from homeassistant.components import ffmpeg
13  PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
14  Camera,
15 )
16 from homeassistant.components.ffmpeg import get_ffmpeg_manager
17 from homeassistant.const import (
18  CONF_HOST,
19  CONF_NAME,
20  CONF_PASSWORD,
21  CONF_PATH,
22  CONF_PORT,
23  CONF_USERNAME,
24 )
25 from homeassistant.core import HomeAssistant
26 from homeassistant.exceptions import PlatformNotReady
27 from homeassistant.helpers import config_validation as cv
28 from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 DEFAULT_BRAND = "YI Home Camera"
35 DEFAULT_PASSWORD = ""
36 DEFAULT_PATH = "/tmp/sd/record" # noqa: S108
37 DEFAULT_PORT = 21
38 DEFAULT_USERNAME = "root"
39 DEFAULT_ARGUMENTS = "-pred 1"
40 
41 CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
42 
43 PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
44  {
45  vol.Required(CONF_NAME): cv.string,
46  vol.Required(CONF_HOST): cv.string,
47  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
48  vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
49  vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
50  vol.Required(CONF_PASSWORD): cv.string,
51  vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
52  }
53 )
54 
55 
57  hass: HomeAssistant,
58  config: ConfigType,
59  async_add_entities: AddEntitiesCallback,
60  discovery_info: DiscoveryInfoType | None = None,
61 ) -> None:
62  """Set up a Yi Camera."""
63  async_add_entities([YiCamera(hass, config)], True)
64 
65 
67  """Define an implementation of a Yi Camera."""
68 
69  def __init__(self, hass, config):
70  """Initialize."""
71  super().__init__()
72  self._extra_arguments_extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
73  self._last_image_last_image = None
74  self._last_url_last_url = None
75  self._manager_manager = get_ffmpeg_manager(hass)
76  self._name_name = config[CONF_NAME]
77  self._is_on_is_on = True
78  self.hosthost = config[CONF_HOST]
79  self.portport = config[CONF_PORT]
80  self.pathpath = config[CONF_PATH]
81  self.useruser = config[CONF_USERNAME]
82  self.passwdpasswd = config[CONF_PASSWORD]
83 
84  @property
85  def brand(self):
86  """Camera brand."""
87  return DEFAULT_BRAND
88 
89  @property
90  def is_on(self):
91  """Determine whether the camera is on."""
92  return self._is_on_is_on
93 
94  @property
95  def name(self):
96  """Return the name of this camera."""
97  return self._name_name
98 
99  async def _get_latest_video_url(self):
100  """Retrieve the latest video file from the customized Yi FTP server."""
101  ftp = Client()
102  try:
103  await ftp.connect(self.hosthost)
104  await ftp.login(self.useruser, self.passwdpasswd)
105  except (ConnectionRefusedError, StatusCodeError) as err:
106  raise PlatformNotReady(err) from err
107 
108  try:
109  await ftp.change_directory(self.pathpath)
110  dirs = []
111  for path, attrs in await ftp.list():
112  if attrs["type"] == "dir" and "." not in str(path):
113  dirs.append(path)
114  latest_dir = dirs[-1]
115  await ftp.change_directory(latest_dir)
116 
117  videos = []
118  for path, _ in await ftp.list():
119  videos.append(path)
120  if not videos:
121  _LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
122  return None
123 
124  await ftp.quit()
125  self._is_on_is_on = True
126  return (
127  f"ftp://{self.user}:{self.passwd}@{self.host}:"
128  f"{self.port}{self.path}/{latest_dir}/{videos[-1]}"
129  )
130  except (ConnectionRefusedError, StatusCodeError) as err:
131  _LOGGER.error("Error while fetching video: %s", err)
132  self._is_on_is_on = False
133  return None
134 
136  self, width: int | None = None, height: int | None = None
137  ) -> bytes | None:
138  """Return a still image response from the camera."""
139  url = await self._get_latest_video_url_get_latest_video_url()
140  if url and url != self._last_url_last_url:
141  self._last_image_last_image = await ffmpeg.async_get_image(
142  self.hasshass,
143  url,
144  extra_cmd=self._extra_arguments_extra_arguments,
145  width=width,
146  height=height,
147  )
148  self._last_url_last_url = url
149 
150  return self._last_image_last_image
151 
152  async def handle_async_mjpeg_stream(self, request):
153  """Generate an HTTP MJPEG stream from the camera."""
154  if not self._is_on_is_on:
155  return None
156 
157  stream = CameraMjpeg(self._manager_manager.binary)
158  await stream.open_camera(self._last_url_last_url, extra_cmd=self._extra_arguments_extra_arguments)
159 
160  try:
161  stream_reader = await stream.get_reader()
162  return await async_aiohttp_proxy_stream(
163  self.hasshass,
164  request,
165  stream_reader,
166  self._manager_manager.ffmpeg_stream_content_type,
167  )
168  finally:
169  await stream.close()
def __init__(self, hass, config)
Definition: camera.py:69
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:137
def handle_async_mjpeg_stream(self, request)
Definition: camera.py:152
FFmpegManager get_ffmpeg_manager(HomeAssistant hass)
Definition: __init__.py:106
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: camera.py:61
web.StreamResponse async_aiohttp_proxy_stream(HomeAssistant hass, web.BaseRequest request, aiohttp.StreamReader stream, str|None content_type, int buffer_size=102400, int timeout=10)