Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Component providing support for Xiaomi Cameras."""
2 
3 from __future__ import annotations
4 
5 from ftplib import FTP, error_perm
6 import logging
7 
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_MODEL,
20  CONF_NAME,
21  CONF_PASSWORD,
22  CONF_PATH,
23  CONF_PORT,
24  CONF_USERNAME,
25 )
26 from homeassistant.core import HomeAssistant
27 from homeassistant.exceptions import TemplateError
28 from homeassistant.helpers import config_validation as cv
29 from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 DEFAULT_BRAND = "Xiaomi Home Camera"
36 DEFAULT_PATH = "/media/mmcblk0p1/record"
37 DEFAULT_PORT = 21
38 DEFAULT_USERNAME = "root"
39 DEFAULT_ARGUMENTS = "-pred 1"
40 
41 CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
42 
43 MODEL_YI = "yi"
44 MODEL_XIAOFANG = "xiaofang"
45 
46 PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
47  {
48  vol.Required(CONF_NAME): cv.string,
49  vol.Required(CONF_HOST): cv.template,
50  vol.Required(CONF_MODEL): vol.Any(MODEL_YI, MODEL_XIAOFANG),
51  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
52  vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
53  vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
54  vol.Required(CONF_PASSWORD): cv.string,
55  vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
56  }
57 )
58 
59 
61  hass: HomeAssistant,
62  config: ConfigType,
63  async_add_entities: AddEntitiesCallback,
64  discovery_info: DiscoveryInfoType | None = None,
65 ) -> None:
66  """Set up a Xiaomi Camera."""
67  _LOGGER.debug("Received configuration for model %s", config[CONF_MODEL])
68  async_add_entities([XiaomiCamera(hass, config)])
69 
70 
72  """Define an implementation of a Xiaomi Camera."""
73 
74  def __init__(self, hass, config):
75  """Initialize."""
76  super().__init__()
77  self._extra_arguments_extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
78  self._last_image_last_image = None
79  self._last_url_last_url = None
80  self._manager_manager = get_ffmpeg_manager(hass)
81  self._name_name = config[CONF_NAME]
82  self.hosthost = config[CONF_HOST]
83  self._model_model = config[CONF_MODEL]
84  self.portport = config[CONF_PORT]
85  self.pathpath = config[CONF_PATH]
86  self.useruser = config[CONF_USERNAME]
87  self.passwdpasswd = config[CONF_PASSWORD]
88 
89  @property
90  def name(self):
91  """Return the name of this camera."""
92  return self._name_name
93 
94  @property
95  def brand(self):
96  """Return the camera brand."""
97  return DEFAULT_BRAND
98 
99  @property
100  def model(self):
101  """Return the camera model."""
102  return self._model_model
103 
104  def get_latest_video_url(self, host):
105  """Retrieve the latest video file from the Xiaomi Camera FTP server."""
106 
107  ftp = FTP(host)
108  try:
109  ftp.login(self.useruser, self.passwdpasswd)
110  except error_perm as exc:
111  _LOGGER.error("Camera login failed: %s", exc)
112  return False
113 
114  try:
115  ftp.cwd(self.pathpath)
116  except error_perm as exc:
117  _LOGGER.error("Unable to find path: %s - %s", self.pathpath, exc)
118  return False
119 
120  dirs = [d for d in ftp.nlst() if "." not in d]
121  if not dirs:
122  _LOGGER.warning("There don't appear to be any folders")
123  return False
124 
125  first_dir = latest_dir = dirs[-1]
126  try:
127  ftp.cwd(first_dir)
128  except error_perm as exc:
129  _LOGGER.error("Unable to find path: %s - %s", first_dir, exc)
130  return False
131 
132  if self._model_model == MODEL_XIAOFANG:
133  dirs = [d for d in ftp.nlst() if "." not in d]
134  if not dirs:
135  _LOGGER.warning("There don't appear to be any uploaded videos")
136  return False
137 
138  latest_dir = dirs[-1]
139  ftp.cwd(latest_dir)
140 
141  videos = [v for v in ftp.nlst() if ".tmp" not in v]
142  if not videos:
143  _LOGGER.debug('Video folder "%s" is empty; delaying', latest_dir)
144  return False
145 
146  if self._model_model == MODEL_XIAOFANG:
147  video = videos[-2]
148  else:
149  video = videos[-1]
150 
151  return f"ftp://{self.user}:{self.passwd}@{host}:{self.port}{ftp.pwd()}/{video}"
152 
154  self, width: int | None = None, height: int | None = None
155  ) -> bytes | None:
156  """Return a still image response from the camera."""
157 
158  try:
159  host = self.hosthost.async_render(parse_result=False)
160  except TemplateError as exc:
161  _LOGGER.error("Error parsing template %s: %s", self.hosthost, exc)
162  return self._last_image_last_image
163 
164  url = await self.hasshass.async_add_executor_job(self.get_latest_video_urlget_latest_video_url, host)
165  if url != self._last_url_last_url:
166  self._last_image_last_image = await ffmpeg.async_get_image(
167  self.hasshass,
168  url,
169  extra_cmd=self._extra_arguments_extra_arguments,
170  width=width,
171  height=height,
172  )
173  self._last_url_last_url = url
174 
175  return self._last_image_last_image
176 
177  async def handle_async_mjpeg_stream(self, request):
178  """Generate an HTTP MJPEG stream from the camera."""
179 
180  stream = CameraMjpeg(self._manager_manager.binary)
181  await stream.open_camera(self._last_url_last_url, extra_cmd=self._extra_arguments_extra_arguments)
182 
183  try:
184  stream_reader = await stream.get_reader()
185  return await async_aiohttp_proxy_stream(
186  self.hasshass,
187  request,
188  stream_reader,
189  self._manager_manager.ffmpeg_stream_content_type,
190  )
191  finally:
192  await stream.close()
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:155
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:65
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)